Tools & Capabilities

dsh-argp

yoza10635/dsh-argp · ★ 15

yoza10635Curated

Deterministic context compaction: prunes conversation atoms in reference-graph topological order with no LLM calls during compaction, and keeps pruned atoms recallable by marker.

License
MIT
Added
2026-08-17
Maintenance
active
Install kind
github
npm
dsh-argp
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-08-19
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add github:yoza10635/dsh-argp

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. dsh-argp — Two-engine context compaction: guarded per-atom shrink + deterministic reference-graph eviction
  2. Why
  3. Core mechanics
  4. Stage-1: PeratomCompressor (eager entropy reduction)
  5. Stage-2: ArgpGraphEngine (lazy graph eviction, 0-LLM)
  6. Three-tier trigger (the Stage-2 trigger ladder)
  7. Bridging and recall
  8. Model requirements (honest version)
  9. Install & mount
  10. Stage-1 (two-engine): mounted by default
  11. Checking status: /argp
  12. Third-party marketplace verification (dsh.so)
  13. Validation results
  14. Four-arm comparison (30-turn synthetic multi-turn coding, local Qwen3.6-35B-A3B, spike 37)
  15. Water level and turn amplification
  16. Graph engine historical validation (v0.3.x, DeepSeek v4-flash / Qwen3.8-27B)
  17. Reproduce
  18. Platform gap feedback (for dsh)
  19. Known limitations
  20. Reporting issues
  21. License

中文 | English

dsh-argp — Two-engine context compaction: guarded per-atom shrink + deterministic reference-graph eviction

CI GitHub Release npm version npm downloads License: MIT

dsh-argp is a third-party context compaction engine for DeepSeek Harness (dsh): it replaces summarizer-style compaction ("let the model rewrite history") — the LLM only proposes, deterministic guards dispose; nothing shrunk or evicted is ever destroyed (append-only log + verbatim recall), and the compression ratio is honored exactly (0-LLM graph rules). Two-engine form, mounted on install with no manual configuration; set peratom: false explicitly to fall back to 0-LLM graph eviction only (see Install & mount).

dsh-argp overview: guarded two-stage context compaction

dsh plugin --profile <name> add dsh-argp    # mounted on install; type /argp in a session to see live status

Three core mechanisms:

  • Stage-1 per-atom shrink (eager, per turn) — at turn end, the turn's atoms are shrunk, not discarded: the model picks extract (verbatim excerpt) / summary (abridged; every dropped token is itemized into an audit ledger) / false (keep original) per atom, and deterministic guards decide whether a proposal lands — an extract missing even one load-bearing token is rejected whole. The LLM only proposes; it never destroys.
  • Stage-2 reference-graph eviction (lazy, three-tier trigger) — on the atom reference graph (deterministic A→R pairing edges + model-declared semantic cites edges), whole atoms are evicted in reverse topological order with zero LLM calls in the eviction phase, so the compression budget is honored exactly; triggering is a three-tier ladder: turn-start proactive / mid-turn pressure prune / post-clamp auto-continue (see "Three-tier trigger").
  • The append-only log is the single source of truth — originals of everything shrunk or evicted stay in the log forever; two-tier recall recall_summary / recall_detail brings them back on demand (text blocks verbatim, hash-locked by tests; tool-call arguments are a JSON semantic-equivalent reconstruction, annotated, when the host stores them as an object; the fidelity guarantee covers structured load-bearing tokens verbatim, not prose-level wording; large nodes page via from/limit). The context is a rendered view of the log, not the history itself.

Why

Summarizer-based compaction (an LLM rewriting history) pays three prices:

  1. Lossy — exact tokens (paths, error codes, config values) die first in any rewrite, and are unrecoverable;
  2. Cache wipeout — rewritten history changes the system+prefix every turn, invalidating cross-turn KV/prefix caches from the change point on;
  3. Uncontrolled ratio — summary length is at the model's whim; the budget is never honored.

ARGP's answer: the LLM stays in the loop, but in chains — its output is always an untrusted input proposal, and guards adjudicate under verbatim discipline; forgetting is deterministic — 0-LLM graph rules guarantee convergence and budget; history is immutable — the append-only log carries every original, backed by the recall contract (never guess).

Measured (30-turn synthetic multi-turn coding task, four-arm comparison, spike 37): the only arm that achieves both 7/7 probe fidelity and lower cost than the active baseline is the dual-engine arm (A); the traditional summary baseline (D) is cheapest but scores 5/7 — it swallows exact strings and key gist. The selling point is not "cheapest" but "cheapest under fidelity" (A's full cost components ≤ baseline C; 3.39× more expensive than D — that gap is the price of fidelity, priced openly).

How it works (dual-engine pipeline, reverse-topological pruning, shadow-price contract, module map) — see ARCHITECTURE.md.

Core mechanics

Stage-1: PeratomCompressor (eager entropy reduction)

  1. Deterministic gating (gate.ts): pure predicates decide whether a turn is compressible (long user messages / tool results over 512 chars / non-version-chain members); the LLM only decides how.
  2. Per-atom decisions: a single LLM call returns {seq, level, text} decisions — extract (verbatim subset; the guard hard-rejects missing tokens) / summary (allowed; every dropped token is itemized into summaryDropped for audit) / false (explicitly keep the original); long user messages go through splitting (dialog verbatim transcription + remainder aggregated as U-info, gaps assigned to info).
  3. No-op guard: full-copy proposals with ≤5% gain count as false — no replace event is emitted (a zero-gain replacement wastes a surface generation for nothing).
  4. Tail-only replacement: replacements must satisfy sourceEventSeqs ⊆ the current turn's range (out-of-range is a bug, locked by assertion); prefix-fingerprint tests prove request-prefix stability across N turns of per-turn compression — the lifeline of cache economy.

Stage-2: ArgpGraphEngine (lazy graph eviction, 0-LLM)

  1. Atomization + graph building: deterministic edges (assistant → its tool results, via toolCallId) + semantic edges (the model outputs {"cites":[{"t":"prefix","l":"c|s|x"}]} per contract, four levels: critical/supporting/contextual/isolated).
  2. Topological eviction: repeatedly evict in-degree-0 atoms (ordered by edge level → effective importance → last-reference turn); evicted atoms' outgoing edges vanish and downstream atoms unlock pass by pass; the closure lifecycle (ACTIVE→COMPLETED→PRUNABLE→PRUNED) retires finished task closures whole.
  3. Budget honored exactly: trigger line window = contextWindow×0.8 (default), retain target = compressible content × 0.2 (since 1.5.1: compressible = full request − fixed system/tools/MCP overhead = the visible prune target, a constant 80% ratio; the old retain = window×0.2 = 0.16W counted fixed overhead into the base, asymmetric with the overhead-free target). The trigger timing is the three-tier ladder (see "Three-tier trigger" below); the degradation chain is lifecycle→force→fail (the summarize tier is a stub that always returns null — conservative option a of A6, not implemented; enableSummarize defaults to false), converging to budget or failing explicitly — measured 200K → 160K trigger → 32K retained, landing exactly.

Three-tier trigger (the Stage-2 trigger ladder)

Stage-2's "lazy" is not a single threshold check but a three-tier ladder (introduced in 1.4.0, revised in 1.5.0):

  1. Turn-start proactive (L1): the pressure check runs only at each turn's first pre-step (step === 1), threshold = window (default contextWindow×0.8). The estimate includes this step's user message that has been claimed but not yet flushed to the surface — the host claims the new user message before dispatching pre-step, so turn start sees the exact content, not a prediction; a large paste at turn start is never missed. Turn start also runs the per-atom LLM pass (atom shrink) + graph eviction.
  2. Mid-turn pressure prune (L1', 0-LLM): midTurnPrune defaults to true — when step > 1 and pressure is reached, only 0-LLM graph eviction runs (no per-atom LLM pass — that is a 79s–3min block, not worth it mid-turn), with turnGuard relaxed to midTurnTurnGuard (default 0, allowing this turn's older A/R to be evicted; recencyGuard still protects the newest nodes). The overage comes from this turn's tool results; the old guard (turnGuard=1) protected the whole turn — the root cause of 1.3.x's mid-turn prune being "nearly ineffective". The prune lands in that pre-step ⇒ the same step's request is already slimmed ⇒ the turn continues naturally.
  3. Post-clamp auto-continue (L3): when the output is externally clamped (finish=max-tokens and outputTokens < the request's maxTokens = the host/adapter shrank the output budget — the true capacity-pressure signal): if the turn is about to end (agent/turn-stopping hook) ⇒ force-prune in place + steer a continuation message, so the same turn keeps advancing (the user need not send "continue"); otherwise (the turn is still running) ⇒ force-prune at the next pre-step. The continuation text can be overridden by continuationNotice (empty string = prune without continuing); the cap is reactiveRetries (default 2, per "consecutive clamps" episode; from the 2nd on, recency/turn guards are relaxed; once exhausted, control returns to the overflow path).

Config knobs: midTurnPrune (default true) / midTurnTurnGuard (default 0) / continuationNotice (default built-in sentence) / reactiveRetries (default 2); 1.4.0's midTurnActive remains as a compatibility alias (true = mid-turn prune on with the default guard, the 1.3.x comparison tier; false = off).

Bridging and recall

  • CiteDeclarer (per turn): the model declares cross-turn reference edges over its window, fed to Stage-2 through the injectEdges channel — measured recall efficiency ≈ 2.6× the edgeless arm (zoom pinpoints the right atoms).
  • RecallZoom: recall_summary(seq) (compressed state) / recall_detail(seq, from?, limit?) (log original: text blocks verbatim, tool-call arguments JSON semantic-equivalent and annotated when stored as an object; large nodes page via from/limit); 4:1 budgeting (summary budget = 4× detail) — over-budget calls return guidance text instead of a hard refusal. Atoms evicted from history are additionally served by recall_pruned / list_pruned.

Model requirements (honest version)

The quality of per-atom split/shrink decisions depends on the model's instruction-following ability; the guards guarantee "never compresses badly" on any model (errors only ever err toward compressing less), but the benefit scales with compliance:

  • Measured baseline: local Qwen3.6-35B-A3B / Qwen3.8-27B, full pipeline over 30/60 turns with 0 errors and 7/7 probes; parse failures of the split-transcription notation 0% (vs 72% for interval locating).
  • Known DeepSeek-family trait: when the system prompt conflicts with user instructions (e.g. a task prompt saying "nothing else"), cites declarations can drop to 0 — semantic selectivity goes to zero, but Stage-1 guarded compression and Stage-2 deterministic eviction keep working and all invariants pass (50-turn v4-flash evidence). Once the task prompt leaves room for cites, the declaration rate recovers (43.6% measured over 10 turns).
  • The information-contract compliance probe (info-contract) will be measured on DeepSeek in the post-1.0.0 recheck round.

Install & mount

Install from npm. npm default = both engines on: the package's bundle patch (cordis.patch.yml) declares maxPasses: 256 / recencyGuard: 10, and Stage-1's three pipelines are mounted by the engine at construction because peratom defaults to mounted (see below):

dsh plugin --profile <name> add dsh-argp

⚠️ Installing within 24 hours of a release: pin the version explicitly. Since pnpm 11, minimumReleaseAge defaults to 1440 minutes (24h): a just-published version is skipped and pnpm silently falls back to the newest version past the cooldown — no error, just the wrong version installed. After each dsh-argp release the older version's peerDeps usually no longer match the current host, so the host reports Plugin dsh-argp@<older version> is incompatible with dsh <host version>. A version number in that error older than npm latest means you hit this cooldown (unrelated to this repo or any mirror: npmmirror and npmjs agree on dist-tags).

Two remedies (the first is narrowest):

dsh plugin --profile <name> add dsh-argp@<latest version>   # ① pin the version, bypassing the cooldown
# ② exempt this package in the profile's pnpm-workspace.yaml (durable; other packages stay protected)
minimumReleaseAgeExclude:
  - dsh-argp

Disabling the stock summarizer already ships with the package: the bundle patch carries - id: compaction-basic / disabled: true, so there is normally nothing to do at the profile layer (this block used to be a manual step; it is kept here only to document who turns it off). Mounting is likewise handled by the package's own bundle patch (insert creates the entry); the profile layer should only override config (modify) — do not insert again there (otherwise duplicate loader entry id).

Inert under the minimal preset (since 1.9.0): ARGP stands down under the presets listed in skipPresets — by default ['minimal'], so sessions running the minimal preset are not pruned or compacted and their history stays pristine (as if this plugin were not installed). The decision follows the preset a session actually runs under (changeable via the preset-switch event); sessions with no preset info (headless/CLI) are unaffected. To make ARGP active under minimal too, add config: { skipPresets: [] } via modify in the profile layer.

Stage-1 (two-engine): mounted by default

The production mount path is the engine self-mounting from config.peratom at construction: peratom defaults to mounted — the three Stage-1 pipelines (compressor / declarer / zoom) are mounted and wired internally at construction, so a fresh install is already the two-engine form with no manual configuration.

To fall back to pure Stage-2 (0-LLM graph eviction), disable it explicitly:

- id: dsh-argp
  config:
    peratom: false      # or null; both mean "Stage-2 only"

Why the default lives in code rather than in the package's bundle patch: patch-layer config is replaced wholesale, not deep-merged (layer order bundle → profile → home → CLI). If the default were written only in the bundle patch's insert row, any settings-page tweak (form values written back to the profile's cordis.patch.yml) would wipe peratom whole and the two-engine form would silently fall back to Stage-2 only — the §11.13.1 gap reasserting itself.

Optionally pin a dedicated LLM backend for Stage-1 (otherwise it follows the host's routing):

- id: dsh-argp
  config:
    peratom:
      compressor:
        llm: { provider: deepseek-official, model: deepseek-flash }   # dsh-llm backend
      declarer:
        llm: { provider: deepseek-official, model: deepseek-flash }   # may point at a separate lite tier
      # zoom: {}   # two-tier recall; mounted by default when the block is present

The llm sub-block is optional: when both llm and endpoint/apiKey are absent, the backend automatically follows the host's routing (1.3.0's autoDshLlmSpec: in a real session it reads agent.options.{provider,model} on the fly + the host's ctx.llm service, following the host when it switches models); when all three paths fail to resolve, the component is naturally disabled (zero network). With llm set, it uses the host dsh-llm (production form); otherwise it falls back to OpenAI-compatible direct connection via endpoint/apiKey config or environment variables (the local llama.cpp experiment form). Stage-2 budgets are ratio-driven by default (window=ctx×0.8 / retain=window×0.2); no hardcoding needed.

presetClean is on by default: at mount time it generates a cleaned copy <id>-argp for each shipped preset that still carries the stock summarizer (removing compaction-basic/tool-result-pruner, keeping command-compact — /compact then routes to ARGP graph eviction); idempotent and fail-soft; set presetClean: false to disable it.

Checking status: /argp

Once installed, type /argp in a session to see ARGP's live status (read-only; it never triggers compaction):

ARGP 1.10.1 · engine: graph+peratom · preset: standard
budget: trigger 131.1k / retain 26.2k · now 88.4k (source=anchored)
prunes: 7 · last 3 ranges / 41 items / 12.8k→3.1k · pruned index 156
recall: 12 calls · 9 hits (75%)
cites: 34 declared · 29 resolved (2 ambiguous, 1 failed)

Line by line: engine — graph = Stage-2 only (0-LLM), graph+peratom = both engines; a preset suffix of ARGP inactive means the current preset is in skipPresets (this plugin does nothing for that session). budget — trigger line / retain target / current usage, where source is how usage was estimated (anchored = pinned to real provider-reported usage, the most accurate). prunes — compaction count and the shape of the last one; forced = pruned after candidates were exhausted. The cites line only appears when the Stage-1 declarer is mounted.

Manual compaction stays on the host's native /compact (provided by command-compact, mounted at the preset layer, which lands on this engine's compactNow) — this plugin adds no second compaction entry point.

Third-party marketplace verification (dsh.so)

dsh itself has no official plugin marketplace (plugin-manager installs straight from npm); dsh.so is a community-run index and verification site. Its badge has two very different layers:

Layer What it checks Site-wide pass rate
L4 npm install inside a sandbox (can deps/peers actually resolve) 100%
L5 web boot in the same sandbox + smoke actions + manifest marked active 74.5%

In other words: installing is never the common problem; booting is (L4 is all green while roughly a quarter of L5 runs fail). A badge shows the most recent result for that dsh version, not a permanent state — red/green across different dsh versions is not directly comparable.

Current status of this plugin on dsh.so: L4 passes, L5.2 (web boot) fails (1.9.0 / 1.9.1 @ dsh 0.2.0-rc.2; last green was 1.7.0 @ rc.1). Reproduced locally against the sandbox signature (win32 x64 / Node 24 / dsh 0.2.0-rc.2 / web profile) across the full matrix — with and without this plugin, checkout and published builds, full and minimal profiles — all green; the failure does not reproduce. The host also exposes no /ping or /health endpoints, so that smoke action is a site-private protocol that cannot be simulated locally. Verdict: not attributable to this plugin; root cause pending data from the site operator. Production deploys use a local tgz plus a hand-mounted profile and never go through the marketplace path, so they are unaffected.

Validation results

Four-arm comparison (30-turn synthetic multi-turn coding, local Qwen3.6-35B-A3B, spike 37)

Arm Configuration Probes Cost (idle pricing) Verdict
A dual-engine full compressor + declarer + graph + zoom 7/7 ¥0.454 The only 7/7 arm at ≤ baseline cost
B edgeless declarer off 7/7 ¥0.556 31 recalls vs A's 12 (declarer ≈2.6× cheaper)
C active baseline graph only (evicts at overflow) 6/7 (R2 missed) ¥0.802 A's full cost components ≤ C
D summary baseline dsh stock BasicCompactionEngine 5/7 (loses exact tokens + gist) ¥0.134 Cheapest but loses fidelity — the foil for "cheapest under fidelity"

Anti-interference: zero replacements of append-origin originals across arms A/B/C (A 140 / B 154 / C 216 events); prefix stability: A's 21 main requests share one fingerprint.

Water level and turn amplification

Measured behavior under a fixed window (16K tok; P5-bis, local Qwen3.6-35B-A3B, 2026-08-28 — evidence detail in CHANGELOG):

  • Turn count amplified substantially: the zero-compaction control dies at ~20 turns on the window ceiling; the dual engine survives to full budget under the same window (~60-turn scale) — sustainable turns under a fixed window improve by an order of magnitude (lower-bound caliber; never cite bare multipliers — always carry window/task/model).
  • Zero per-request prefix-stability degradation: the dual engine's per-request prefix fingerprint distribution matches the zero-compaction control; compaction events add only a one-time recompute tax, no cumulative degradation.

Graph engine historical validation (v0.3.x, DeepSeek v4-flash / Qwen3.8-27B)

50-turn t-long: U anchors 7/7, needles 7/7 (5/7 recovered via recall), 4 transactions 0 errors, compression target honored exactly (32K); 200K mainline cost ¥2.695 vs baseline high ¥3.087 (that baseline contains 77% empty-stream errors — a platform B-5 defect; read the contrast under this caliber, the disabled tier ¥3.19 is the cleaner control).

Reproduce

Experiment Command What it validates
Four-arm comparison (needs a local model) ARGP_ARM=A|B|C|D|E node spike/37-peratom-three-arm.ts Probe fidelity, cost triplet, anti-interference, K_no / amplification
per-atom soak npm run spike36 Eight verdicts: gating / chain / conservation / prefix / VK-atom
50-turn t-long ARGP_DEEPSEEK_THINKING=enabled node spike/06-tlong.ts L1/L2/L3 invariants, anchors/needles, exact budget
Synthetic 0-LLM npm run spike8a Single transaction with zero LLM calls
Per-atom audit node spike/atom-audit.mjs <artifact dir> Event-driven per-atom shrink/eviction detail

npm run check = typecheck + smoke + unit tests (415/415 green as of 2026-10-12). Every number carries its artifact path (evidence landing in CHANGELOG.md).

Model caliber for the historical DeepSeek arms: the v0.3.x DeepSeek arms were measured with deepseek-v4-flash — that model was retired in 2026-09; the official API now serves deepseek-flash (DeepSeek-V4.1-Flash), with the old name kept only as a compatibility route (the host llm-deepseek catalog is now deepseek-flash / deepseek-v4-pro). The numbers above are artifacts of that model and cannot be reproduced as-is on the new API; re-running the DeepSeek arms uses deepseek-flash (the shared spike/deepseek.ts mount has migrated along, overridable via DEEPSEEK_MODEL).

Platform gap feedback (for dsh)

No structured metadata channel for tool/result replacement (B-1), compaction/prune outside the transaction state machine (B-3), headless test assembly silently disables tokenMeter (B-4), summarizer empty streams (B-5), window truncation leaves no trace (B-6) — the formal record lives in dsh Discussions (#1090 and related threads).

Known limitations

  • B-6 window-truncation blind spot: live nodes not replaced by ARGP get their oldest part silently truncated by the request-assembly layer as the context nears contextWindow — recall_pruned cannot recover them. Mitigation: ratio budgets trigger earlier; the root fix is on the dsh side (B-6 filed upstream).
  • Model dependence (honest version, above): guards guarantee safety; benefits depend on compliance. The lite-tier multi-model split's compliance is unmeasured (ledger D21).
  • per-atom output tax: Stage-1's per-turn compression call is a side-channel cost (7.2K completion tokens over 30 turns, measured; it never enters the context but counts toward total cost); usage from the dsh-llm backend is recorded, and the spike aggregation caliber is being wired up.
  • Tombstone recall path: since 1.8.0 every tombstone carries its origin node's seq ([elided seq=N; recall_pruned(N) for detail]), so recall_pruned(seq) recovers it in one hop — no more list_pruned --keyword reverse lookup; and "re-tombstoning" is excluded as a terminal state (G1), so the original seq is no longer diluted by chained replacements. Still bounded by the B-6 window-truncation blind spot (unreplaced live nodes remain unrecoverable).

Reporting issues

  • Bugs: open an Issue with dsh --version, this package's version, your profile config (windowTokens/retainTokens etc.), and a minimal reproduction.
  • Design discussion / usage questions: open a Discussion.

License

MIT