Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add @yeastcloud/dsh-anchor
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
Screenshots
README
Anchor · 定锚
Inject a composable instruction block into every DeepSeek Harness session — and re-anchor the same text after a compaction or a long run.
Persona, tone, working discipline, project rules: say it once, and it keeps counting.
The problem
You open a session with an instruction block: answer in Chinese, lead with the conclusion, ask before destructive commands, keep code minimal. It works for a few turns. Then:
- turn 40 — the block sits at the very bottom of the context and the model starts to forget it;
- after
/compact— a summary swallows it. Summaries describe what happened; they never repeat how you want things done; - inside a long tool loop — dozens of tool results push it further away, and both style and discipline drift.
dsh-anchor drops an anchor on that instruction block: inject it once at session start, then re-inject the same text whenever the session is compacted, crosses the turn interval you configured, or crosses a context-token threshold.
Features
| Feature | What it does |
|---|---|
| 🧩 Composable presets | Build up to 100 presets, check any subset, and inject them joined in the order you drag them into. |
| ⚓ Re-anchor after compaction | Watches for compaction/summary (automatic pressure compaction or /compact) and re-anchors at the next step boundary — including mid-turn, so the very next request carries it. |
| 🔁 Re-anchor by turn count | Re-anchors at the start of a turn once N turns (default 20, configurable, 0 disables) have passed since the last injection. |
| 📈 Re-anchor on token pressure | Reads the official token meter: once the context figure — the same one shown beside the composer — reaches the token count you set (reinjectTokenThreshold, 0 = off by default), it re-anchors at the start of the next turn. It fires once per crossing, so a context that merely stays high is not re-anchored turn after turn; a compaction that drops the figure back below the threshold starts a new crossing. |
| 🎯 Selectable re-anchor source | Three ways: first keeps repeating the session's opening text; latest repeats the newest injection of this plugin (for example one you anchored later with /anchor); refresh re-anchors the combination the settings page holds right now — edit a preset or change the selection and the running session picks up the new text at its next re-anchor. |
| 🧠 Replayable triggers | The compaction and turn-count triggers read the durable session log and nothing else, in pure functions: restart, resume and replay reach the same decision, and one trigger can never fire twice. The token trigger reads the official meter instead (itself a replay of that log) and keeps one armed flag per session — see "How it works". |
| 🚦 Configurable limits | Per-preset authoring limit and a combined injection gate, both editable in the settings page (default 8000 chars). Above the gate the plugin refuses to inject and logs it instead of silently truncating your text. |
| 🐋 Subagents stay unanchored | Delegated child sessions get no anchor and no re-anchor by default: the persona and discipline are for the session you steer yourself, and a child replaying them only spends tokens. Turn it on in the settings page to treat children the same. |
⚓ /anchor command |
Type /anchor to anchor the current combination into this session right away: the handler runs locally against the agent, so it costs no tokens, opens no turn, and never extends a turn already running (the anchor is held by the plugin and injected at the first step of your next message — it never enters the composer and never opens a turn). /anchor status reports without injecting. |
| 🎛 Self-drawn settings page | Paged preset library, ordered injection list with drag/↑↓ reordering, and the re-anchor policy — all visual, no config file editing. |
| 💾 Presets import / export | Export a JSON document carrying its own format marker and version, including the library order, the injection order and every setting (all 8 of them) — for another machine, a backup, or sharing. An import is a merge, and it never destroys: presets that do not clash come in checked, a preset whose id is already stored with different content is a conflict (left unchecked, i.e. keep yours, with the current and imported versions shown side by side), and the settings are listed in the three zones Injection / Re-anchoring / Character limits, checked per item and switchable per zone. A selectedIds entry that resolves nowhere is no longer a refusal — it is listed as an unresolvable item and left out. The injection order is a row of its own (checked, and cancellable), so a file that only extends the combination still imports. Only a structurally broken file (not JSON, not ours, an unknown revision, a malformed structure) is refused whole, with one named reason. v1 documents still import, and their absent settings section never overwrites your own. |
| 🔒 Local only | No network, no telemetry. Its whole state stays on your machine: the dsh-anchor section of ~/.dsh/settings.yaml on the 0.1.6 line, the active profile's configuration document (entry id dsh-anchor) from 0.1.7 on. |
DSH version compatibility
| DSH version | Anchor plugin version |
|---|---|
0.2.0 (including 0.2.0-rc.x, verified working on this machine) |
0.14.0 (current) |
0.1.7 (including 0.1.7-rc.1 / 0.1.7-rc.2) |
0.14.0 (current) |
| 0.1.6 (verified working on this machine) | 0.11.0 |
| 0.1.5 (covered by the peer range, unverified) | 0.11.0 |
- The 0.2.0 line is verified working on this machine (2026-09-30): the same
0.14.0serves both lines, hence both rows point at it.- 0.13.0 requires peers
>=0.1.7-rc.1— it no longer supports the 0.1.6 line (0.1.7 replaced the settings service, the client settings surface and the v4 session format; seeCHANGELOG.md).0.11.0declares^0.1.5-rc.2(verified viagit show 6358c90:package.json): by node-semver rules that range only admits the0.1.5tuple.0.1.6-alpha.2is not covered by that declaration but has been running here for a long time — hence the two separate rows.
Install
# Requires DeepSeek Harness (Web profile)
dsh plugin --profile web add @yeastcloud/dsh-anchor
# The host half loads at boot: restart the profile
dsh web
Then open Settings → 定锚 (Anchor), add a few presets, check them, drag them into the order you want, and start a new session.
Requirements: Node ^22.19 || >=24; the DeepSeek Harness 0.1.7-rc.2 line (that is what the dependency and peer ranges name); the 0.2.0 line is verified working on this machine (see the table above). The plugin ships a host half (injection logic) and a client half (settings page).
Where the settings live: on 0.1.6 the host half registered a section with ctx.settings.installSection and the document was ~/.dsh/settings.yaml. That API is gone from 0.1.7, where the form is generated from the plugin's own Config schema and the values follow the active profile's configuration document — under an entry id equal to the plugin namespace (dsh-anchor), which is also what lets the platform's one-shot import of the retired section land on this plugin.
Language: the settings page and its left-nav entry follow the DSH language setting (中文 / English); /anchor output follows the host process's LC_ALL / LANG (a tag whose first segment is en — en, en_US.UTF-8 — reads English, anything else Chinese).
Commands
Type /anchor in any session:
| Command | What it does |
|---|---|
/anchor |
Anchors the current combination into this session: one plugin-sourced message is injected and takes effect at the first step of your next message (it never enters the composer, never opens a turn, and never extends a turn already running), and every later compaction / turn-interval re-anchor reuses it. The handler runs locally on the receiving agent, so it costs no tokens and produces no model reply. |
/anchor status |
Read-only report: master switch, combination and length, re-anchor source / turn interval / token threshold / after-compaction switch, this session's injection history (first / latest seq and length, plus anything still queued), and how many turns passed since the last injection. Injects nothing. |
Every refusal path reports an error instead of injecting something approximate: switch off, empty combination, or a combined length above the configured limit.
Why a command: the command contract states the handler runs locally against the agent and the command is not sent to the model — which is exactly what "drop an anchor on this conversation" should be. That is also why the earlier composer button, and with it the whole "recognise it by content digest" mechanism, is gone: the command delivers the same text without spending tokens, opening a turn, or needing to recognise itself.
How it works
- Session start — on the first step of the session's own first turn (
turn === 1), the plugin prepends onesource.kind = "plugin"user message carrying the combined text. It is model-visible and logged, and it is distinguishable from a real submission. - Re-anchor — if the log shows a completed summarizing compaction after the last injection, the next step boundary re-injects (mid-turn included). If N turns have passed, step 1 of the next turn re-injects. If the context pressure crossed the configured token threshold, step 1 of the next turn re-injects too.
- Token pressure — the one trigger whose input is a measurement rather than a log event. It reads
contextPressurefrom the official token meter (dsh-token-meter) throughctx.sessionProjections: the same figure the composer renders, itself computed by replaying the session log. The rule is edge-triggered: a reading below the threshold re-arms a per-session flag, a reading at or above it fires once and disarms, so a context that merely stays above the threshold is not re-anchored again. A compaction that drops the reading back below re-arms it, and the next crossing fires. A profile without the meter reads nothing and never fires. The flag lives in the process: after a restart every session is armed again, so a session that is still above the threshold gets one more re-anchor at the next turn's first step, and the once-per-crossing rule holds from then on. - Replay-safe by construction — every injection moves the reference seq forward, so a log-driven trigger fires once; nothing but the token trigger's armed flag lives outside the durable log. Sessions that were never anchored are never given a belated "opening line" mid-conversation.
- Manual anchoring —
/anchorholds the combination on the host and injects it at the first step of your next message, so the command costs no tokens, opens no turn and extends no running turn.
Configuration
Edit in Settings → 定锚, or in the settings document itself: the dsh-anchor section of ~/.dsh/settings.yaml on the 0.1.6 line, or the entry with id dsh-anchor in the active profile's configuration document (dsh config / the profile's cordis.patch.yml) from 0.1.7 on:
| Field | Default | Meaning |
|---|---|---|
enabled |
true |
Master switch: off stops both anchoring and re-anchoring |
selectedIds |
['default'] |
Ordered combination; an empty array means "inject nothing" |
prompts |
1 item | Preset library, up to 100 items, name ≤ 200 chars |
reinjectAfterCompaction |
true |
Re-anchor after a summarizing compaction |
reinjectTurnInterval |
20 |
Turn interval between re-anchors (0–10000, 0 disables) |
reinjectTokenThreshold |
0 |
Token threshold for the pressure trigger (0–10000000, 0 disables). Reads the official token meter, where the figure is the same context occupancy the composer shows; crossing it from below re-anchors once at the next turn's step 1, and staying above it does not re-anchor again |
maxPromptChars |
8000 |
Authoring limit per preset; never truncates stored text |
maxCombinedChars |
8000 |
Injection gate for the combined text; above it nothing is injected and a warning is logged |
reinjectSource |
'first' |
Which injection a re-anchor repeats: first, latest or refresh (the current combination) |
anchorSubagents |
false |
Whether delegated (subagent) sessions are anchored too — off by default |
FAQ
Why not inject every turn? Repeating the block every turn burns context and desensitises the model to it. The anchor fires exactly when information is lost: after compaction, and after your configured turn interval.
Can the context decide for itself instead of counting turns? Yes — set reinjectTokenThreshold to a token count (120000, say). The crossing of that figure (the one beside the composer, from the official token meter) re-anchors once at the start of the next turn, once per crossing: staying above the threshold never re-anchors again, while a compaction that drops it back below starts a new crossing. The default is 0 (off), and with it the plugin behaves exactly as it did before this trigger existed — it does not even read the meter.
Does re-anchoring fight the compaction summary? No. The summary records what happened; the anchor restates how you want things done.
What happens if I anchor with /anchor in latest mode? That anchor becomes the session's newest injection, so later compaction and turn-interval re-anchors repeat it — a persona switch for the session. Switching back to first returns to the opening text.
What counts as an injection? Only messages this plugin sourced itself: the opening anchor, a compaction or turn-interval re-anchor, and an anchor you placed with /anchor. Text you type or paste never counts, and no content comparison is involved.
Does it send anything anywhere? No. It never touches the network; the only thing it writes is its own settings document (the dsh-anchor section of ~/.dsh/settings.yaml on the 0.1.6 line, the dsh-anchor entry of the active profile's configuration document from 0.1.7 on).
Roadmap
Finished items stay listed (✅ + strikethrough) with the release that shipped them.
| Planned | Shipped in | Date | |
|---|---|---|---|
| ✅ | v0.7.0 |
2026-09-15 | |
| ✅ | v0.9.0 |
2026-09-15 | |
| ✅ | v0.10.0 |
2026-09-15 | |
| ✅ | v0.11.0 |
2026-09-15 |
No planned items remain.
Development
pnpm install # dependencies (prepare builds once)
pnpm check # typecheck + 152 tests + build
pnpm build # lib/index.js (host) and lib/client.js (client)
Client-half changes take effect on a page refresh; host-half changes need a profile restart.
Releasing
gh workflow run release.yml -f bump=minor # bump and release
gh workflow run release.yml -f bump=none # release the version already in the repository
gh workflow run release.yml -f bump=patch -f dry_run=true # verify the pipeline only, nothing is committed or published
The workflow bumps the version, runs pnpm check, commits and pushes an annotated tag, publishes to npm, and creates a GitHub Release. Publishing uses npm trusted publishing (OIDC) — no npm token is stored in the repository, and every release carries a provenance attestation.
mode must match the permission granted to this package's Trusted Publisher on npmjs:
mode |
npm permission required | Behavior |
|---|---|---|
stage (default) |
npm stage publish |
the version is staged on npm and becomes public once a maintainer approves it with 2FA: npm stage list <pkg> → npm stage approve <stage-id> |
direct |
npm publish |
live as soon as the workflow finishes |
The very first publish is manual (npm requires the package to exist before a trusted publisher can be configured); afterwards every release goes through the workflow.
License
CC BY-NC-SA 4.0 © 2026 YiSiYun Studio (一笥云)
Granted: non-commercial use, modification, and redistribution under the same license. Required: attribution (keep the copyright and license notice, and state changes). Prohibited: any commercial use — contact YiSiYun Studio for a commercial license.
Note: this is a source-available, non-commercial license, not an OSI-approved open-source license. The plugin comes with no warranty.
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.