DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

yangyu666 /

yangyu666/dsh-jev-prune

Verified

Jev-judged context compaction for DeepSeek Harness: semantic tool-result pruning + deterministic receipt compaction

★ 3 Stars2 Forks3 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@d7add2c7

dsh-jev-prune

dsh-jev-prune — Jev-judged context compaction for DeepSeek Harness

Jev-judged context compaction for DeepSeek Harness. Structured judgments from TypeSafe Jev drive DSH's two-layer context compaction. The compaction algorithms are untouched; the judgment backend is pluggable (Jev / rules / a self-hosted model).

English · 简体中文

license node dsh CI smoke checks

The problem it solves

DSH's built-in context reclamation is purely volumetric. Once a tool result crosses a size threshold, its middle is chopped out and the head and tail are kept; region compaction, meanwhile, has the model write a summary to stand in for old history. The first approach cannot tell "this result is large but I still need it" from "this one is spent", and the second one invites summary hallucination.

This plugin replaces the decision in both places with Jev's structured output (noul / choice, returning calibrated probabilities), under one design rule:

What should not be generated by a model is not generated by a model. Trimming only ever decides keep or discard; the original text is preserved verbatim. Region compaction injects a deterministic receipt produced by code, containing no model inference at all.

The two layers

The two layers: result trimming and receipt compaction

Layer Interception point DSH default This plugin
1 · Result trimming ctx.toolResultPruner.pruneSession Chops the middle once thresholdChars is exceeded Jev decides, per tool result, whether it will still be needed. Needed ones are never trimmed, however large; stale ones are trimmed however small (unless shorter than minCharsToPrune); with no judgment available it falls back to DSH's original behaviour
2 · Receipt compaction ctx.compaction.summarize + compactRegion The model reads the raw history and writes a summary Moves spent read-only probes (whole tool-call + tool/result pairs) out of the surface and injects a deterministic receipt: tool name, command, path, character count and seq are all computed by code

A layer-2 receipt looks like this:

[已压缩 · 确定性回执] 原历史 s25–s27 是 1 次工具调用(共约 16489 字符输出),
为释放上下文已移出。以下为事实清单(代码生成,无模型推断):
· s27 read:C:\Users\you\project\src\state.js → 16489 字符输出
原始事件仍完整保存在会话日志中(seqs 25–27)。需要内容时重跑相同命令/读取相同文件即可。

The receipt body is emitted by the plugin's own JavaScript, so its wording is Chinese today — as is the /jev status output. The jev_* tool descriptions are already English. Localizing the runtime strings is a separate change.

Gating (layer 2)

Moving a whole pair out of the surface is destructive, so the default is deliberately conservative. Every one of the following must hold:

  • Intersection of two axes: result (is the content still needed) and effect (did the call change state outside the session) must each fall inside this session's trailing compactQuantile
  • The tool is not in neverCompactTools (write-type calls are excluded by a hard rule, never by a probability)
  • Evidence guard: results matching error / assert / fail / todo and friends are never moved out. If layer 1 has already trimmed a result, the guard follows sourceEventSeqs and scans the original event too
  • Steps whose assistant text exceeds maxStepTextChars, or whose reasoning exceeds maxStepReasoningChars, are never moved out. The two are measured separately on purpose: long text means the step is delivering a conclusion worth keeping, while long reasoning is just scratch work — merging them into one budget let reasoning length alone silently shut layer 2 off
  • Anything within the most recent compactPreserveRecent nodes is skipped by layer 2 (layer 1 uses preserveRecent)
  • Both ends of the range must satisfy DSH's tool-pairing balance; the span must save at least compactMinChars characters; and the receipt must stay below receiptMaxRatio of the original content's tokens

Probabilities are consumed as relative quantiles, never as a fixed threshold: the output distribution of a small judge model is narrow, and only the relative ordering within one session carries stable information.

Degradation on small populations. Read-only tools are often a minority in write/execute-heavy sessions (measured: 1 in 6), which can leave a quantile population of only two or three items — too few for ordering to mean anything. Rather than giving up, the mode degrades to an absolute floor: both axes must fall below floorThreshold (default 0.2, materially stricter than compactThreshold, compensating for the missing relative information). If the population is below minCandidatesForFloor (default 2), nothing is moved out — a single sample is not a distribution. The default was lowered from 3 to 2 so that the two-candidate populations that batch-read sessions really produce are not skipped outright; one sample still never acts. Degradation is always reported in the report and the heartbeat; it never happens silently.

Receipt ownership (fence)

Layer 2 injects through a short pipeline: the plugin renders a receipt → stores it in pendingReceipt → calls compactRegion → DSH calls back into summarize, where the receipt is handed over.

The trouble is that compactRegion is asynchronous, and summarize receives no range identity — it cannot tell which compaction it is being invoked for. So if anything else (say DSH's own automatic compaction) starts a compaction while we are awaiting, both share the same session-keyed slot: that other span gets replaced by our receipt, and the span we meant to compact falls back to a model summary. Both histories are corrupted, and neither side reports an error.

The fix issues an ownership token (fencing token) per compaction, with three constraints:

Mechanism Effect
Ownership token The producer bumps fenceCounter and records activeFence; summarize injects only when entry.fence === activeFence
Claim-once entry.claimed latches, so repeated summarize calls within one compaction cannot double-inject
Ownership check After compactRegion returns, the token is verified; if it was taken over we set action.fenceLost = true and never report a false success

finally clears only our own token — the original implementation deleted the session entry unconditionally, which wiped another compaction's pending receipt. Takeovers are counted in receiptFenceMisses and surfaced in the status report when non-zero; in that case the span falls back to a model summary, which is the safe direction.

Requirements

  • Node ^22.19.0 || >=24.0.0
  • dsh (@deepseek-ai/dsh), with the base bundle loaded into the profile (tool-result-pruner and compaction-basic are included by default)
  • A TypeSafe API key (TYPESAFE_API_KEY environment variable)
  • Runtime peer dependencies: @deepseek-ai/schemastery, @deepseek-ai/dsh-tools (provided by the host)
  • Optional dynamic dependency: freezeMessage from @deepseek-ai/dsh-llm (falls back to a shallow copy when absent; the plugin keeps working)
  • Host services consumed: toolResultPruner, compaction, tools, commands, llm, and tokenMeter. tokenMeter is used by the pressure gates; if your host does not register it, ratio-based gating cannot compare anything — set softLimit / compactSoftLimit to an absolute token count, or use judgeOn: 'always' / compactOn: 'always' (see Pressure-gate failure direction). A missing meter never silently disables a layer: the gate is left un-armed for that pass and the reason is logged at warn level.

Version alignment: the peer range for @deepseek-ai/dsh-tools is ^0.1.5-rc.2 — the tested version 0.1.5-rc.2 sits on npm's next tag, not latest. A lockfile is committed (devDependencies pin the tested versions), so npm ci reproduces the exact test conditions.

Compatibility: tested against @deepseek-ai/dsh@0.1.5-rc.2. DSH 0.1.x is a pre-release line, and event shapes and service names can shift between rc versions. After upgrading DSH, re-run npm run check and the smoke test, and call jev_probe_shapes once in a real session to verify the field assumptions.

Project documentation: contributing, architecture, porting contract, and configuration examples.

Install

# Install from a local directory (the repo directory name matches the package name)
dsh plugin --profile web add link:/absolute/path/to/dsh-jev-prune

# Confirm it made it into the config tree
dsh --profile web --dump-config | grep jev-prune

On a machine without pnpm, the equivalent manual wiring (idempotent) is:

node wire_profile.mjs <DSH_HOME> <profile-name>

Configuration

Key Default Description
enabled true Master switch
model jev-latest Judge model
keepMode budget Layer 1 decision rule. budget: how much to trim is set by the pressure-gap ratio, which results by Jev's ranking (see below). absolute: the legacy fixed-threshold behaviour
keepThreshold 0.5 Layer 1: in absolute mode, P(keep) ≥ this means no trimming; in budget mode it is a protection ceiling only (results at or above it never enter the candidate pool)
alwaysTrimRatio 0.5 Layer 1: fixed trim ratio used only under judgeOn: 'always' (that mode has no pressure signal to derive one from). The budget is this fraction of the candidate pool's total character gain. pressure mode computes the ratio from the gap and ignores this key
volumeBudgetThresholdChars 8192 ⚠️ Deprecated (kept only for compatibility): early budget mode anchored the budget on the volume rule; it now uses the pressure-gap ratio instead, so this key no longer takes effect
keepFloorThreshold / minCandidatesForBudget 0.2 / 4 budget mode small-population fallback: with fewer than 4 judged candidates, only results with P(keep) < 0.2 are eligible (same degraded-mode shape as layer 2)
budgetMinChars 0 ⚠️ Deprecated (kept only for compatibility): same as above, no longer takes effect
resultExcerptChars 240 Layer 1: per-result excerpt budget copied into the judge's state (see below); 0 restores the blind ok, N chars line
preserveRecent 4 Layer 1 leaves the most recent N surface nodes alone
headChars / tailChars 600 / 200 Layer 1: how many head/tail characters a trim keeps
minCharsToPrune 400 Layer 1: anything shorter is never trimmed
judgeOn / softLimit pressure / 55% Layer 1: when to judge, and the pressure line
compactReceipts / compactOn true / pressure Layer 2: switch and pressure line (compactSoftLimit, default 70%)
compactMode relative relative (recommended) or absolute (with compactThreshold)
compactQuantile 0.34 The trailing fraction taken on each of the two axes; the intersection is used
compactPreserveRecent 1 Layer 2 leaves the most recent N surface nodes alone; independent from layer 1's wider recent window
minCandidatesForRelative 4 Minimum population for relative quantiles; below it the mode degrades to an absolute floor (see below) rather than giving up
floorThreshold / minCandidatesForFloor 0.2 / 2 Absolute floor used in the degraded mode (materially stricter than compactThreshold) and its minimum sample size
neverCompactTools write-type tools Layer 2 never moves these out; comparison is normalized (Edit ≡ edit)
neverPruneTools Write / NotebookEdit Layer 1 never touches these. Narrower than the row above on purpose: layer 1 only truncates (reversible, the original stays in the session log), so the arguments of diff-style editors (Edit/ApplyPatch…) are fair game; layer 2 removes the pair outright, so it keeps guarding all of them
compactTools read-only set Allow-list, non-empty by default (DSH_READONLY_TOOLS: read/glob/grep/list/fetch… plus PowerShell read-only cmdlets such as getchilditem/selectstring). Setting it to [] relaxes the gate to the deny-list only — shell calls then become movable too, which is an explicit opt-in into an unsafe mode
evidenceGuard / evidencePatterns true / built-in list Evidence guard
compactMinChars / receiptMaxRatio 2000 / 0.5 Layer 2 economical floors
maxCompactionsPerPass 3 How many compaction transactions one pass may run. Raised to 3 so a large context converges in a single pass instead of being squeezed across many pre-steps; set to 1 for the old behaviour
judgeMaxRetries / judgeRetryBaseMs 2 / 300 Retry count and backoff base for judge requests (see below); 0 disables retries
dryRun false Both layers only judge and account; nothing is changed
heartbeatFile '' Where to persist state (the host swallows plugin logs, so a file is the only external observation channel)

Retrying judge requests

A single network hiccup used to void the entire round of judging — no candidate got a probability and both layers silently did nothing. Failures are now classified:

Failure Handling
Network error / timeout Retry with exponential backoff (300ms → 600ms, up to 2 retries by default)
429 / 5xx Retry (server temporarily unavailable)
Other 4xx (401 bad key, 400 malformed request) No retry — immediately fatal; retrying only burns quota
Response missing answers No retry (a retry would most likely return the same broken body)
External signal already aborted No retry, and no new request is issued

Batches are isolated too: one failed batch no longer discards the remaining ones, and the count shows up as 失败批次 N 个 in the status report. Only when every batch fails is the round treated as failed.

Counter semantics (clarified in the PR #28 review): client.lastRetries is reset at the start of every ask and is what the status report shows, while client.retries is the lifetime total for the client (useful for "has this client ever had to retry?"). client.requests counts HTTP attempts actually issued, including failed ones, so the identity requests === successful asks + retries holds. Using the lifetime counter as if it described the current pass would make the report show 重试 N 次 forever after a single hiccup, with N only ever climbing.

Token-estimate accuracy

estimateTokens is a heuristic (the plugin ships no tokenizer), but its constants are no longer guesses: they were grid-searched against a real BPE tokenizer over 22 samples (English prose, camelCase identifiers, JSON, Windows and Unix paths, git diffs, Chinese, mixed Chinese/English, code blocks, logs, pure punctuation, hex/UUID, table rows, single glyphs, whitespace), scoring on a weighted fit + holdout objective to avoid overfitting.

Mean absolute error drops from 20.5% to 10.7% (holdout 20.5% → 14.4%), and the direction was corrected: the old formula over-estimated pure English by +37% and Unix paths by +44%, and since both layers use this value in a ratio, it was tightening both gates. The new estimate is essentially unbiased (−0.3%).

npm run check asserts accuracy against a holdout set (5 samples that took no part in the fit, with reference lengths measured from the real tokenizer; current MAE 5.5%): a hard MAE ≤ 15% bound plus a directional assertion that English prose must not be over-estimated. This distinction matters (corrected in the PR #28 review): the first version reused the calibration data itself, which made the assertion a tautology — it could only catch "someone hand-edited the constants", never "the constants overfit the fitting set". With a real holdout, pushing wordSlope to 0.9 jumps the MAE to 38.7% and fails immediately. (Also: the holdout reference lengths must be measured with the real tokenizer, not estimated — 4 of the 5 values I first hand-wrote were off by more than 10%, i.e. the assertion would have been built on wrong numbers.)

Pressure-gate failure direction

Both layers fail closed and in the same direction: when the threshold itself cannot be computed (a ratio soft limit with an unresolvable context window), neither layer acts.

The old behaviour was asymmetric — layer 2 skipped when it could not resolve a threshold, while layer 1 simply fell through and proceeded; more subtly, a missing meter left used at 0, so 0 < threshold was always true and judging ran every single round, i.e. the gate did not exist. For a gate whose purpose is to avoid spending Jev calls, "if we cannot tell, do not spend" is the safe direction.

The boundary (corrected during the PR #28 review): failing closed justifies declining to spend, but it must not turn into silently switching the feature off. When the soft limit is an absolute token count (softLimit: 3000), the threshold comes straight from limit.value and the meter is irrelevant — so if the meter is missing or throws, the gate is simply left un-armed for that pass (reported as 压力门本次不设防 at warn level) and judging proceeds. An earlier revision of this PR required a successful measurement unconditionally, which turned "stop wasting money" into "the first layer never runs again" for any host that does not register tokenMeter — strictly worse than the bug it was fixing. smoke_apply.mjs pins both directions.

Note that tokenMeter is a host-provided service; if your host does not expose it, configure softLimit as an absolute token count (or set judgeOn: 'always' / compactOn: 'always') rather than relying on ratio-based pressure gating.

Host compaction threshold vs. softLimit

Layer 1 does not schedule pruneSession itself. The host's compaction-basic bundle calls it when the host reaches its own pressure threshold (thresholdRatio, 0.8 by default) or on context overflow. This plugin's softLimit controls when Jev judging starts and how large the trimming budget is; it does not replace the host threshold.

In pressure mode, a layer-1 trim therefore needs both conditions:

host calls pruneSession
AND
used tokens exceed softLimit (so the pressure-gap budget is greater than zero)

Keep softLimit at or below the host's thresholdRatio unless the delayed behaviour is intentional. For example, with host thresholdRatio: 0.8 and plugin softLimit: 90%, host calls between 80% and 90% produce a zero plugin budget; trimming starts only after usage reaches 90%. With the default softLimit: 55%, judging is ready before the host's normal 80% compaction call.

For @deepseek-ai/dsh-llm-deepseek@0.1.5-rc.2, configure a smaller context window on the matching model entry:

- id: llm-deepseek
  config:
    models:
      - id: deepseek-flash
        contextWindow: 10000

Setting only defaultContextWindow does not override catalog models that already carry their own contextWindow; the model entry wins. If the plugin cannot resolve the effective window, ratio-based gates stop and report the reason instead of guessing.

Layer 1: pressure-quantile trimming

The layer-1 decision used to be a bare fixed threshold: keep = P(keep) ≥ 0.5. Live-host measurement broke that assumption: every judged candidate scored below 0.5 (42/42 in a 132k-token session, 5/5 in a short one; median ≈ 0.13–0.17). Jev's probabilities live in a narrow band — the exact trap layer 2 had already escaped by switching to relative quantiles, except nobody applied the lesson to layer 1. Under the fixed threshold, the first layer's real-world behaviour was "trim everything that was judged", including results the session still needed.

budget mode (the default) decouples the two questions:

  • How much to trim comes from the pressure-gap ratio: ratio = (used − threshold) / window, computed automatically each judge pass (the fraction of the context window that is over the soft limit); the budget is ratio × the pool's total character savings. Zero gap ⇒ trim nothing; the closer to the ceiling, the more is trimmed.
  • Which results comes from Jev: candidates are sorted by P(keep) ascending and trimmed until the budget is met. Results with P(keep) ≥ keepThreshold (0.5) are a protection ceiling and never enter the pool; candidates whose savings are already counted stop there — the rest are recorded as 预算已用尽 (keptByBudget) rather than silently kept or trimmed.
  • With a small population (< minCandidatesForBudget), the mode degrades to an absolute floor (keepFloorThreshold, 0.2) instead of inventing a ranking from 2–3 samples — the same degraded-mode shape layer 2 uses.

The old behaviour stays available as keepMode: 'absolute'.

Result excerpts (giving the judge eyes)

The judge's state used to describe every tool result as ok, 16489 chars (内容省略) — the judge knew that something big existed but not what was in it. Blind judging plus a fixed threshold degenerates into "trim whatever is large".

With resultExcerptChars (default 240), each result line in the state carries a bounded excerpt. Lines are picked by informativeness, not position, because the two naive rules both failed a real-session A/B:

  1. error/evidence-pattern lines (the obvious candidate), plus
  2. salient lines: constant identifiers (THRESHOLD_DISCOUNT_PCT, E2001_BASE_IMAGE), assignments/keys (timeout = 4800), file paths — the critical config line in a 20 KB module is neither at the head nor an error, and rule 1 alone missed it (measured: judging outcomes identical to no excerpt at all), plus
  3. a middle-line fallback for pure-prose results (the middle is exactly what "cut the middle" loses).

The excerpt is hard-bounded per result and counted against the state budget, so it cannot blow up the request size. One trade-off to know: excerpts share the fixed maxStateTokens budget with history lines — at ~70 tokens per excerpted result, a 100-result session spends ~28% of the default 25k-token budget on excerpts, and the squeeze logic compensates by dropping more history lines. If you run very long sessions, raise maxStateTokens (Jev's ceiling is 32k) or lower resultExcerptChars rather than disabling excerpts entirely. Note the interaction with budget mode: excerpts shift probabilities; only the ranking-based decision converts better information into different trimming. Under a fixed threshold both A/B arms behaved identically — the excerpt's value presupposes the ranking rule.

Judgement observability (heartbeat)

The heartbeat now records decision evidence, not just counters — because both the 0.5-threshold failure above and the upstream #25–#29 regressions were invisible in a stats-only heartbeat:

Field Content
keep Layer-1 decision rule as configured (mode, ceiling, floor, budget parameters)
gate Last pressure-gate evaluation: used / resolved window / threshold / skip + reason / candidate count
probSummary Distribution of P(keep): p10–p90, mean, counts above/below the ceiling
probSamples Last 200 raw probabilities (for histograms)
lastJudgePass.rows Per-candidate detail: seq, tool, chars, prob, effectProb
stats.preStepEvents / stats.judgePassSkipped + lastJudgeSkipReason Distinguishes "the event never fired" / "no candidates" / "gate skipped" — three failures that used to look identical from outside

One structural fix came out of this: the heartbeat is merge-written, but the pre-step hook itself never called writeHeartbeat — so gate-skipped passes left the file frozen at the boot snapshot (bootedAt == now) and every skipped path was unobservable. The hook now persists after every step.

Out-of-range configuration

Every numeric option has a valid range, and out-of-range values are never passed through to the runtime:

  • Through the Config schema (the host's normal load path) → a ValidationError is thrown. A loud refusal.
  • Without schema normalization (a config object injected directly by cordis.patch.yml, or the PLUGIN_CFG built by the smoke test) → resolveConfig falls the value back to its default (not to the nearest bound, because "how far off was it" isn't interpretable), and records a configWarnings entry in the status report and heartbeat.

These are the real consequences, all of which used to happen silently:

Setting Behavior before the fix
preserveRecent = -5 lastAllowed grew instead of shrinking → recent-node protection completely defeated (in-flight tool calls could be touched)
maxStepTextChars = -1 every step judged "text too long" → layer 2 permanently and silently dead
compactMinChars = -100 the gate ceased to exist
receiptMaxRatio = 5 a receipt 5× larger than the original was allowed through (safety gate defeated)
keepThreshold = 2 layer 1 pruned everything (prob >= 2 is never true)

Note that 0 is a legal value for most keys (headChars = 0 keeps no head; preserveRecent = 0 protects nothing) — it is not treated as "unset".

In-session usage

Entry point Purpose
/jev, jev_prune_status Ledger for both layers: cached judgments, cumulative savings, takeover state, tool-name index
jev_prune_now Force one layer-1 trimming pass
jev_compact_now (supports dryRun) Force one layer-2 receipt compaction and list every gate's exclusion counts; in dryRun it also prints the full receipt text
jev_restore Safety valve: fetch back the original text that a checkpoint moved out (read-only)
jev_probe_shapes Print the real event shapes and the resolved tool names, for adapting to a different DSH version

In normal operation both layers are driven automatically by context pressure; no manual step is needed.

Design notes

  • Reads probabilities, never generated text: judgments always read answers[id].noul from the structured response
  • The state carries the task goal: "is this still useful" really means "useful relative to the goal", so the state header carries the most recent user instruction
  • Structure by code, semantics by the model: write-type calls and the recent window are guaranteed by hard rules, never entrusted to a probability
  • The shadow-price protocol is aligned verbatim with DSH's compaction/prune + surfaceOp: replace, so pure consumers can reuse the same token accounting
  • Slices by Unicode code point, never splitting a surrogate pair; token estimation uses a per-word correction algorithm that works for mixed CJK/Latin text
  • Hook ordering is load-bearing: the judge hook is prepended (ctx.on(..., true)) so it runs before the base bundle's compaction-basic. That package is the only caller of pruner.pruneSession (both call sites live in it — :888 for context-overflow, :902 for pressure), so it is also the only place layer 1's verdicts get consumed. Registered without prepend, pruning would read the previous round's verdicts and every fresh result would fall back to the size rules — layer 1 silently inert, no error anywhere. smoke_apply.mjs block M pins this by observing the judge counter at the moment pruneSession is called.

Testing

npm install   # pull the peer dependencies (a lockfile is committed; CI uses npm ci)
npm run check # pure-function self-checks: token estimation / state assembly / candidate selection / both layers' decisions / packaging completeness

The smoke test (no full DSH dependency tree needed, ~4 s). Note that the plugin entry statically imports two peers, so in a clean directory install them first (the failure message says the same):

npm install @deepseek-ai/schemastery @deepseek-ai/dsh-tools
cp {index,jev,state,prune,receipt}.js package.json <some-dir>/node_modules/dsh-jev-prune/
cp smoke_apply.mjs <some-dir>/ && cd <some-dir>/ && node smoke_apply.mjs

The test scripts and helper tools (check.js / smoke_apply.mjs / inspect_session.mjs / verify_real_shapes.mjs / wire_profile.mjs) all ship with the npm package, so a plain npm run check works inside an installed copy. CI (.github/workflows/ci.yml) runs two jobs: a fast smoke job on the peer dependencies alone, and an integration job on the full DSH dependency tree.

Coverage: the takeover of both interception points, the full decision path of both layers, the append protocol, receipt injection and its ownership (fence), concurrent-compaction races, every gating branch (with counterfactual controls), the text/reasoning split, small-population degradation, out-of-range config clamping, judge retries and per-batch isolation (including per-pass vs lifetime counter semantics), non-duplicated batch accounting, pressure gates failing closed in the same direction while still acting when the threshold is an absolute count, token-estimate calibration against a holdout set, the compaction quota, alwaysTrimRatio actually moving the budget (with a precondition assert that the run took the budget path and not the small-population fallback), a missing session exiting gracefully instead of throwing, the judge hook being prepended ahead of the base bundle's compaction-basic (observed at the exact moment pruneSession is called), a skipped layer-2 pass recording why it was skipped (the blocked reason plus the per-reason exclusion counts — previously only the success path wrote a note, so the one path you actually need to debug was the one that stayed silent), the degraded-mode floor pinned at both readings (the mechanism, with the floor passed explicitly, and the default — the exported constant is now the single source of truth, so it can no longer diverge from computeEligibleSeqs's own defaults), and shell-type tools being excluded by default (the pwsh Remove-Item regression case).

The fake ctx in smoke_apply.mjs mirrors cordis's listener model, not just its method names: multiple listeners per event, prepend, and waterfall ordering — where a listener that never calls next() vetoes the rest of the chain, including the host's built-in behaviour. Modelling it as a one-handler-per-event map hid the ordering contract completely: two listeners silently overwrote each other, and the prepend flag was ignored.

Test boundaries (what CI actually verifies): pure-function logic, takeover and the append protocol under a fake ctx, plus — in the integration job — "the plugin module loads against the real dependency tree and freezeMessage is available". Not covered by CI: service takeover inside a live DSH host and event-shape drift between rc versions — verify those with jev_probe_shapes in a real session.

Repository layout

├── index.js            # Plugin entry: config, both interception points, pre-step orchestration, commands and tools
├── prune.js            # Layer 1: code-point slicing, per-node decisions, shadow-price protocol
├── receipt.js          # Layer 2: tool-pairing balance, range selection, evidence guard, receipt rendering
├── state.js            # DSH events → judge state: goal extraction, the two question axes, event-shape probes
├── jev.js              # Jev client (structured noul batch API) + token estimation
├── check.js            # Pure-function self-checks
├── smoke_apply.mjs     # Smoke test (real apply on a fake ctx)
├── wire_profile.mjs    # Manual install path for machines without pnpm
├── inspect_session.mjs # Offline inspector for session logs (multi-frame zstd JSONL)
├── verify_real_shapes.mjs # Offline regression of tool-name resolution against real session logs
├── assets/             # Banner and diagrams (SVG sources + rendered PNGs)
└── cordis.patch.yml    # Install contract

Privacy

The plugin sends session history text — including file paths, code snippets and command output — to the TypeSafe API for judgment. Assess this yourself before working on sensitive code. If data must not leave the machine, swap the judgment backend for a self-hosted model: the judgment and the compaction machinery are decoupled, and the replacement points are jev.js and state.js.

License

MIT


English · 简体中文

—/ 5

No ratings yet

Verified DSH bundle

Commit d7add2c7f55e

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout