dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Dead-loop / repetition detector for DeepSeek Harness agents — hard-interrupts text loops and same-topic search retries that soft reminders miss.
Dead-loop / repetition detector with hard interrupt for DeepSeek Harness (dsh).
Local LLMs (especially quantized / RL-tuned reasoning models) sometimes get stuck:
repeating the same sentence forever, or re-running web_search with reworded queries
for the same topic without gaining any new information. This plugin watches the
agent's stream in real time, detects both kinds of loops, and interrupts them
hard — not a soft reminder the model can ignore.
中文文档见 README.zh.md.
v2.0.0 targets dsh 0.2.x. If you installed v1 on dsh 0.2.x and it never fired, that is expected — see the next section.
v1 was written against an older dsh. Three things broke, each fixed in v2:
| Problem | v1 | Reality on 0.2.x | v2 |
|---|---|---|---|
| Never sees streamed text | listened to session/event → assistant/chunk | assistant/chunk is no longer a live v2 session event (only the legacy-format migration packages read it) | listens to agent/assistant-stream |
| Interrupt does not interrupt | used agent.steer() as the interrupt | steer() only queues a message for the next step; it does not abort the current generation | agent.cancel(cause, { keepInbox: true }) first, then steer() to guide |
| Retry budget never runs out | reset the counter on turn/end | every steer() opens a new turn, so the counter was cleared as soon as it grew | resets on genuine user input (plugin-injected messages are tracked by id and excluded) |
v1's intent detector also tokenized on "non-alphanumeric", which collapses a whole Chinese sentence into a single token — so Chinese reworded-search loops were never detected. v2 uses "stop-character splitting + CJK bigrams + Jaccard similarity".
agent/assistant-stream for
text-delta and (optionally) reasoning-delta, with three layered detectors:
--------, |:---|:---|) are not
mistaken for a loop.tools/post-execute, extracts keywords
from each call's arguments, and compares them against the union of the last 3 calls.
A low novelty ratio or a high Jaccard similarity, twice in a row, means
"same task, reworded". toolPatterns takes wildcards, so grep, shell commands, etc.
can participate.maxRetries the whole turn is cancelled.dsh ships @deepseek-ai/dsh-repeat-tool-reminder, but it is advisory only (injected
as context). In practice local models (e.g. Qwen / Ornith 35B MoE with reasoning off)
ignore it and keep looping — which is exactly why this plugin exists.
Prerequisite: a running dsh profile (dsh 0.2.x, Node ≥ 22.18 — the bundled runtime qualifies).
cd $env:USERPROFILE\.dsh\profiles\desktop
pnpm add link:"D:\path\to\dsh-loop-detector"
Dependency-resolution contract: every runtime dependency of this plugin (
@deepseek-ai/cordis,dsh-agent,dsh-llm,dsh-tools,schemastery) is declared inpeerDependencies. For alink:-ed plugin, dsh's profile resolver routes a bare specifier to dsh's own installed copy only when the plugin'spackage.jsonlists that name underpeerDependencies. Do not move these intodependencies: Node would then look inside the plugin's ownnode_modules, and a miss fails the whole plugin load.
Copy-Item -Recurse -Force "D:\path\to\dsh-loop-detector" `
"$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-loop-detector"
The plugin ships dsh.bundle.patch (pointing at this repo's
cordis.patch.yml), which is its self-mount patch. So all you need is to
add the package name to the profile's dsh.profile.bundles:
// $env:USERPROFILE\.dsh\profiles\desktop\package.json
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
// …existing entries…
"dsh-loop-detector" // ← add this line
]
}
}
Restart dsh: the plugin now shows up under Settings → Plugins → Installed, where the toggle
can enable or disable it. (pnpm add link: alone makes the plugin work, but it will not
appear on the plugins page — that list is exactly this bundles array.)
To override defaults, put an id-targeted override at the end of the profile's
cordis.patch.yml; it survives plugin upgrades:
- id: loop-detector
config:
minLen: 1024
checkReasoning: false
To disable temporarily, use the GUI toggle, or:
- id: loop-detector
disabled: true
Then let a model loop on purpose. The log shows:
[loop-detector] session xxx 正文本命中: periodic repeat: period=8 x4|片段 "abcdefgh"
[loop-detector] session xxx 检测到死循环(重试 1/1): periodic repeat: period=8 x4;已打断并引导重试
[loop-detector] session xxx 意图级重复: repeated web_search intent: novelty=0.29 similarity=0.71
| Field | Default | Meaning |
|---|---|---|
minLen | 512 | Start detecting only after this many characters |
maxRetries | 1 | Interrupt-and-guide attempts; then the turn is cancelled |
action | 'steer' | 'steer' = interrupt + guided retry; 'cancel' = cancel on first hit |
cooldownMs | 1500 | Cooldown after a hit (residual chunks must not re-trigger) |
checkText | true | Detect plain text output |
checkReasoning | true | Detect reasoning output |
checkEvery | 64 | Run the detector at most once per this many new characters |
bufferLimit | 16384 | Trailing characters kept per stream (bounded memory) |
| Field | Default | Meaning |
|---|---|---|
toolPatterns | ['web_search'] | Tools to watch (wildcards); empty array disables the feature |
historySize | 3 | How many recent calls form the topic keyword set |
noveltyThreshold | 0.25 | Novel-keyword ratio below this counts as "no new information" |
similarityThreshold | 0.5 | Jaccard similarity at or above this counts as the same topic |
stallLimit | 2 | Consecutive stalled calls before it is a loop |
| Field | Default | Meaning |
|---|---|---|
minPeriod / maxPeriod | 8 / 256 | Period scan range |
periodRepeats | 4 | How many identical periods make a loop |
ngramSize / ngramRepeats | 64 / 3 | n-gram block size and minimum repeats |
ngramWindow / ngramStep | 2048 / 16 | n-gram window and sampling stride |
anchorSize / anchorWindow | 512 / 8192 | Anchor length and window |
minDistinctChars | 2 | Minimum distinct characters in a repeated unit |
minSubstantiveRatio | 0.3 | Minimum substantive-character ratio in a repeated unit |
legacySessionEvents | false | Old-dsh compatibility channel (session/event → assistant/chunk) |
Invalid configuration (unknown action, minLen < 1, ratios outside [0,1]) fails loudly
at mount time instead of silently falling back.
Text level (src/detector.ts):
1) period scan tail repeats a period of [minPeriod..maxPeriod] for periodRepeats times
2) n-gram the same ngramSize-char block appears >= ngramRepeats times in ngramWindow
3) anchor trailing anchorSize chars reappear earlier and extend >= anchorSize back
every layer additionally requires a "meaningful" unit: >= minDistinctChars distinct chars
and substantive-character ratio >= minSubstantiveRatio
Tool-intent level (src/intent.ts):
keywords = latin words + CJK bigrams split on stop characters
topic = union of the last historySize calls' keywords
novelty = novel keywords / current keywords
similarity = |intersection| / |union| (Jaccard)
novelty < noveltyThreshold OR similarity >= similarityThreshold -> one "stall"
stallLimit consecutive stalls -> intent-level loop -> interrupt
CJK bigrams exist because whitespace tokenization is meaningless for Chinese; the extra similarity criterion exists because rewording in Chinese naturally introduces new bigrams.
npm test # node --test runs .ts natively (Node >= 22.18 type stripping)
39 tests cover all three detectors (hits and non-hits: markdown rules, ordinary prose,
short text), Chinese/English keyword extraction and intent decisions, configuration
validation, and — using a real cordis Context and the real dsh-agent scoped dispatcher —
the wiring of agent/assistant-stream / agent/pre-step / tools/post-execute plus the
"interrupt + guide, then cancel" behaviour.
Running the tests requires @deepseek-ai/cordis, @deepseek-ai/dsh-agent and
@deepseek-ai/dsh-llm to be resolvable. A bare node --test never goes through dsh's
profile resolver, so this repo may contain a git-ignored node_modules junction pointing at
dsh's installation node_modules — it exists only for local tests. At runtime dsh uses
its own resolver and does not depend on it.
New-Item -ItemType Junction -Path .\node_modules `
-Target "$env:LOCALAPPDATA\Programs\dsh\resources\app\dsh\node_modules" # your dsh install dir
minLen between 256 and 512.toolPatterns (e.g. '*') may false-positive on legitimately varied tool use.MIT — see LICENSE.