Pular para o conteúdo principal
H

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.

Instalar

dsh plugin --profile web add github:hjdd14/dsh-toolong-warning

README

dsh-toolong-warning

English | 中文

License: MIT dsh Node

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.

The floating reminder window

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.

#ConditionDefault
1Completed 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
2Extra spend since the last compaction — billed tokens (uncachedInput + cacheRead + cacheWrite + output), accumulated from the usage samples the session log carries.≥ 200,000
3Either 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

SituationResult
A very long, very expensive conversation that has never been compactedsilent — only the counter shows (0)
Two compactions only (default threshold is 3)silent
Three compactions, but the window is roomy and nothing extra was spentsilent
Three compactions, 250k extra spend, low occupancy — past the threshold but not past the doubled onesilent
No usage data at all (even at 95% occupancy)silent
The route declares no context window and the extra spend is lowsilent

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, or desktop for 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:

  1. Open the app and add @hjdd14/dsh-toolong-warning from its Plugins page.
  2. Or by hand: add the package to <DSH_HOME>/profiles/desktop/package.json under both dsh.profile.bundles and dependencies, run pnpm install in 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.

FieldDefaultRangeMeaning
enabledtruetoggleWhen off, no state route is registered and no window appears
compactCountMin31–20Completed compactions required before a warning is possible
occupancyPercentMin5010–95Context occupancy above which the conversation counts as expensive
tokensSinceCompactionMin20000010000–5000000Billed tokens spent after the last compaction before it counts as heavy waste; at twice this value the warning fires even without an occupancy figure
statsIntervalMs150005000–120000How often the window refreshes (polling pauses while the page is hidden)
dismissibletruetoggleWhether Got it may hide the current reminder
historyReadTtlMs300005000–600000How long a history read for a session the host has not loaded is reused
historyReadTimeoutMs50001000–30000Time 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-top variable, 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 atThe three decision thresholdsenabled / dismissible
http://127.0.0.1:3080 or http://localhost:3080 (loopback)written to the profile configuration — applies to every sessioneditable, written to the profile
anything else (IP, hostname, tunnel)still editable, stored in this browser, applied to this page onlyread-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

#ConditionResponseNotes
1The session is live in this processsource: "live"Full live metrics, including occupancy from the meter
2Not loaded, but its log is readablesource: "history"Count and spend folded from the log — this is why switching conversations shows a number without a page reload
3Neitherknown: false + reasonsession-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=1 makes 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
Surfacethe 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 relacionados