Skip to content
dsh-market Browse plugins GitHub 中文

yeastcloud/dsh-anchor

Session instruction anchor for DeepSeek Harness: the first step of a new session injects a freely composed instruction block as a plugin-sourced message, and it is re-anchored after a summarizing compaction, every N turns, or when metered context pressure crosses a threshold. An /anchor command re-anchors on demand without a model call; a settings page manages the paged preset library, injection order, re-anchor source and character limits.

Stars ★ 2 Category Sessions & Messages Listed 2026-09-18 npm @yeastcloud/dsh-anchor

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.

CI

中文 · Changelog · Issues


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.0 serves 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; see CHANGELOG.md).
  • 0.11.0 declares ^0.1.5-rc.2 (verified via git show 6358c90:package.json): by node-semver rules that range only admits the 0.1.5 tuple. 0.1.6-alpha.2 is 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 one source.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 contextPressure from the official token meter (dsh-token-meter) through ctx.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 — /anchor holds 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
✅ Settings-page i18n: the UI and the command output follow the DSH locale (zh / en) v0.7.0 2026-09-15
✅ A third re-anchor source that refreshes the current combination v0.9.0 2026-09-15
✅ Preset import / export (sync your instruction library across machines) v0.10.0 2026-09-15
✅ Trigger on context-token pressure, as a second yardstick next to the turn interval (pending confirmation that the harness exposes a token-accounting seam; drop the item if it does not) 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.

Content from the project README on GitHub ↗

Comments

Comments live in GitHub Discussions. Sign in with GitHub to post or react.