- Accueil
- Plugins
- Utilisation et facturation
- dsh-toolong-warning
dsh-toolong-warning
hjdd14/dsh-toolong-warning
DSH plugin: counts completed compactions per session and raises a floating reminder — with bilingual UI — only after repeated compactions plus heavy extra token spend.
Installer
dsh plugin --profile web add github:hjdd14/dsh-toolong-warningREADME
dsh-toolong-warning
English | 中文
A DeepSeek Harness (dsh) plugin that warns when a conversation has become too long to be worth continuing — and stays quiet otherwise.
It counts how many times each conversation has been compacted, shows that count in a floating window at all times, and adds a warning only when compaction has happened repeatedly and the conversation has since burned a lot of extra tokens.
The point of this plugin is what it does not say. A reminder that cries wolf gets ignored, so every rule below is written to prefer silence whenever the data to justify a warning is missing.
Table of contents
What you see
A self-owned floating window in the top-right corner. It uses the shell.overlay seat and falls back
to its own fixed container, so it never competes with another plugin for a slot:
┌──────────────────────────────┐
│ Long-conversation reminder – │
│ This conversation compacted │
│ 3 time(s) │
└──────────────────────────────┘
┌──────────────────────────────┐
│ ⚠ This conversation is │
│ getting long — consider │
│ starting a new one │
│ context occupancy has │
│ reached 62%; about 310k │
│ tokens were spent since the │
│ last compaction │
│ Compacted 3 time(s) so far │
│ (threshold 3); current │
│ context occupancy 62% │
│ [Got it] [Adjust thresholds]│
└──────────────────────────────┘
- The counter is always shown; the warning card appears only when the rule is met.
- Got it hides the current reminder only — the counter keeps updating.
- Adjust thresholds jumps to this plugin's section on the Settings page.
- The window is draggable: press anywhere on the card and move it, so it does not have to sit over the page's own top-right controls. Where you leave it is remembered in this browser, it is kept inside the viewport (and pulled back after a resize), and the Settings section offers a reset row once it has been moved.
- Nothing is shown when the plugin is disabled or no conversation is open.
- The whole UI is bilingual (中文 / English), switchable from the plugin's own settings section — see Settings.
When does it warn?
All three conditions must hold. If any input is missing, the plugin stays silent.
| # | Condition | Default |
|---|---|---|
| 1 | Completed compactions — only a compaction/start paired with a compaction/end that carries no error. Failed compactions, in-progress compactions, and pure pruning (compaction/prune) never count. | ≥ 3 |
| 2 | Extra spend since the last compaction — billed tokens (uncachedInput + cacheRead + cacheWrite + output), accumulated from the usage samples the session log carries. | ≥ 200,000 |
| 3 | Either context occupancy (the next request's expected prompt ÷ the model's context window) or double the spend threshold — the second is the fallback for routes that declare no context window, which is why it is twice as hard to reach. | ≥ 50% or ≥ 400,000 |
Cases where it deliberately stays silent
| Situation | Result |
|---|---|
| A very long, very expensive conversation that has never been compacted | silent — only the counter shows (0) |
| Two compactions only (default threshold is 3) | silent |
| Three compactions, but the window is roomy and nothing extra was spent | silent |
| Three compactions, 250k extra spend, low occupancy — past the threshold but not past the doubled one | silent |
| No usage data at all (even at 95% occupancy) | silent |
| The route declares no context window and the extra spend is low | silent |
A documented example: a session with 2,100+ events, ~87M tokens spent and ~60% occupancy reported no
warning at all, because it had never been compacted (gate: "compactions").
Install
Requirements
- dsh
>= 0.1.7-rc.1(@deepseek-ai/dsh) - Node
>= 22.19 - A profile with a browser client:
web, ordesktopfor the Electron app (this plugin has a browser half; it is a no-op on surfaces without one)
1. From npm
dsh plugin --profile web add @hjdd14/dsh-toolong-warning
2. From the Git repository
dsh plugin --profile web add "git+https://github.com/Hjdd14/dsh-toolong-warning.git"
This plugin ships hand-written JavaScript with no build step, so pnpm has no
prepare/postinstall script to approve and the install does not stop for a build prompt. If a dsh
version reports a pending build script, add the key it prints under allowBuilds in
<DSH_HOME>/profiles/web/pnpm-workspace.yaml and re-run.
3. From a clone (development)
git clone https://github.com/Hjdd14/dsh-toolong-warning.git
dsh plugin --profile web add "link:<absolute path to the clone>"
Editing client.js reloads the plugin in the browser within ~500 ms. Editing host-side files
(index.js, src/*.js) requires a new dsh web process — see Diagnostics.
4. By editing the profile by hand (when the CLI is unavailable)
In <DSH_HOME>/profiles/web/package.json:
{
"dsh": {
"profile": {
"bundles": ["...", "@hjdd14/dsh-toolong-warning"]
}
},
"dependencies": {
"@hjdd14/dsh-toolong-warning": "^0.3.0"
}
}
Then run pnpm install inside <DSH_HOME>/profiles/web and restart dsh web.
5. Desktop (the Electron app)
The DSH Desktop app is another surface over the same installation, with its own profile
(desktop) and the same Web UI served from the app's own origin. It consumes client
plugins that declare platform: "web" — which is exactly what this package declares —
so nothing about the manifest changes; the package is simply installed into the
desktop profile:
- Open the app and add
@hjdd14/dsh-toolong-warningfrom its Plugins page. - Or by hand: add the package to
<DSH_HOME>/profiles/desktop/package.jsonunder bothdsh.profile.bundlesanddependencies, runpnpm installin that directory, then restart the app.
dsh plugin --profile desktop add … is refused on purpose: an application-owned profile
is written through the app's own in-process plugin manager rather than the CLI. The
floating window knows the shell owns the window chrome — see
Desktop placement.
Install a prebuilt tarball
npm pack # produces hjdd14-dsh-toolong-warning-0.3.0.tgz
dsh plugin --profile web add "file:<path to the tgz>"
Uninstall
dsh plugin --profile web remove @hjdd14/dsh-toolong-warning
Or disable it from the dsh web sidebar's Plugins page.
Settings
The plugin's section on the Settings page holds the language switch and every threshold. Values are written into the profile's cordis patch and take effect immediately.
| Field | Default | Range | Meaning |
|---|---|---|---|
enabled | true | toggle | When off, no state route is registered and no window appears |
compactCountMin | 3 | 1–20 | Completed compactions required before a warning is possible |
occupancyPercentMin | 50 | 10–95 | Context occupancy above which the conversation counts as expensive |
tokensSinceCompactionMin | 200000 | 10000–5000000 | Billed tokens spent after the last compaction before it counts as heavy waste; at twice this value the warning fires even without an occupancy figure |
statsIntervalMs | 15000 | 5000–120000 | How often the window refreshes (polling pauses while the page is hidden) |
dismissible | true | toggle | Whether Got it may hide the current reminder |
historyReadTtlMs | 30000 | 5000–600000 | How long a history read for a session the host has not loaded is reused |
historyReadTimeoutMs | 5000 | 1000–30000 | Time limit for one history read; on timeout the window reports that the session cannot be read yet |
The lower part of the section shows the current session's measured values (compactions, billed tokens, spend since the last compaction, occupancy) and why the reminder is or is not firing — which is what calibrating the thresholds needs.
Language
A 中文 / English selector in the same section. The choice is stored in the browser, applies to the
floating window and the settings section immediately (no reload), and is also pushed to the harness'
shared locale service when that service is writable — so on most deployments the rest of the UI
follows too. The global language control DSH itself ships lives in Settings → General.
Window position
Drag the floating window anywhere; the position is stored in this browser, next to the language choice and the threshold override, and is deliberately not a profile setting — it describes one person's screen, not the conversation. It stays inside the viewport (a moved window is pulled back after a resize or when the warning card makes it taller), and a Reset window position row appears in this section as soon as the window has been moved.
Desktop placement
A desktop shell owns the window chrome, and the floating window is aware of it:
- The shell's reserved band is never usable space. The window's default top and the smallest top
it can be dragged to both come from the frame's own
--dsh-frame-overlay-topvariable, minus whatever the shell has already reserved by moving its content viewport down. A shell that layers its chrome over the content and one that moves the content below the chrome therefore each get the right answer, and neither is compensated twice. On a plain browser page no such variable is published, so every value stays what it was. - The space it is clamped against is measured, not assumed. A shell whose content viewport starts below its command bar would otherwise both mis-clamp the window and displace it a little on every drag; an inert probe spanning the containing block measures it exactly. On a browser page the probe measures the viewport itself, so the Web behaviour is unchanged.
- It stays out of the shell's app-region computation, so the window's own drag strips keep working (macOS and Windows Electron both mark the document; a plain page never does).
One consequence worth knowing: browser storage is per origin, so the desktop app and a page at
http://127.0.0.1:3080 keep separate language choices, window positions and browser-local
threshold overrides. That was already true before this adaptation and is not changed by it.
Two scopes of edit — and why
DSH fixes settings writes to read-only on a non-loopback page
(persistence = ctx.remote.$host.isLoopback ? 'host' : 'memory'), and that applies to every
plugin's configuration form, not just this one. Rather than showing dead controls, this plugin
degrades honestly:
| Page opened at | The three decision thresholds | enabled / dismissible |
|---|---|---|
http://127.0.0.1:3080 or http://localhost:3080 (loopback) | written to the profile configuration — applies to every session | editable, written to the profile |
| anything else (IP, hostname, tunnel) | still editable, stored in this browser, applied to this page only | read-only (they decide whether the host registers the route) |
Browser-local values ride each state request as query parameters, are re-validated and clamped by the host on every request, and are never persisted. Out-of-range or non-integer values are ignored and reported back. The decision always happens on the host, so a page cannot force a warning.
How it works
browser (client.js) host (index.js + src/*)
──────────────────── ─────────────────────────
floating window ctx.on('session/event') ── incremental fold (live)
├─ session catalog → main-view session └─ first ask seeds that session's whole log
├─ poll /api/.../state every 15 s ctx.sessionProjections
└─ counter line + warning card └─ tokenUsage / contextPressure
settings section ctx.sessionQuery.readSession(id) ── history (cold)
├─ language switch └─ sessions the host has not loaded
└─ ctx.configForms('toolong-warning') two loopback-only, read-only routes
8 volatile fields /api/dsh-toolong-warning/state (per session)
/api/dsh-toolong-warning/health (probes)
Three data sources, degrading in order
| # | Condition | Response | Notes |
|---|---|---|---|
| 1 | The session is live in this process | source: "live" | Full live metrics, including occupancy from the meter |
| 2 | Not loaded, but its log is readable | source: "history" | Count and spend folded from the log — this is why switching conversations shows a number without a page reload |
| 3 | Neither | known: false + reason | session-not-loaded or history-unavailable; never warns |
- The browser cannot see a session's event log (the client side only has the session catalog), so every measurement happens host-side and is served over a same-origin route.
- Both sources share the same folding and decision functions, so they must agree;
?selftest=1makes the running process prove that on real sessions. - The history source uses the supported read-only service and never reads private storage paths
(
$DSH_HOME/storages/**, session log files). It never inserts a session into the live tracker, is cached with a TTL, and is bounded by a timeout. - The browser half is plain JavaScript registered through
dsh.client+exports["./client"]; React comes from the shell's module table. It is deliberately self-contained: DSH serves client bundles from an exact combo URL out of an in-memory response map, so a sibling module can never be fetched and the dictionaries are inlined. - The host half imports no
@deepseek-ai/*runtime package except@deepseek-ai/schemastery, which the Settings form requires to be a real schema; it is loaded from an explicit candidate path and degrades to a structurally equivalent descriptor when unavailable.
Diagnostics
Both routes are loopback-only (socket address and Host header and same-origin markers) and
always send cache-control: no-store.
# Is it loaded, which generation is running, what are the effective thresholds?
curl.exe -s "http://127.0.0.1:3080/api/dsh-toolong-warning/health"
# Drive the real routes in the running process and compare against expected values.
# Runs two self-checks: a synthetic session through the whole decision chain, and a
# live-vs-history count comparison on real sessions.
curl.exe -s "http://127.0.0.1:3080/api/dsh-toolong-warning/health?selftest=1"
# One session's decision and raw numbers. debug=1 adds event-type statistics and the
# compaction lifecycle — never message content.
curl.exe -s "http://127.0.0.1:3080/api/dsh-toolong-warning/state?sessionId=<session id>&debug=1"
state fields: known, source (live / history), count, lastCompactionSeq,
tokensSinceCompaction, billedTokens, occupancyRatio / projectedTokens / contextWindow,
shouldWarn, reasons, gate, thresholds, thresholdsSource, configVersion, sampledEvents,
droppedEvents, coldReads.
moduleGeneration is load-bearing. Host-side changes only load in a new dsh web process — dsh
hot-reloads client bundles only. The generation counter exists so the running process can report
which code it is serving, rather than that being a matter of inference.
Requirements
| dsh | >= 0.1.7-rc.1 |
| Surface | the web profile, and the Desktop app's desktop profile (the browser half is a no-op elsewhere) |
| Optional | @deepseek-ai/dsh-session-query — enables the history source. Without it, sessions the host has not loaded simply stay unmeasured |
| Host dependency | @deepseek-ai/schemastery ~3.18.4 for the Settings form. The bare schemastery reachable from a profile is 3.18.0 and has no .volatile(), which silently makes the form unavailable |
License
MIT © 2026 Hjdd14
Plugins associés
DeepSeek-Balance-Whale-Widget
meteornox/deepseek-balance-whale-widget
dsh-context
bowenliang123/dsh-context
dsh-balance-plugin
yxxbc/dsh-balance-plugin
dsh-balance-plugin
francis-xavier-code/dsh-balance-plugin