liangmianya/dsh-synapse
liangmianya/dsh-synapseDeepSeek Harness 的可视化非线性对话工作区:把会话、追问与分支变成可浏览、可拖拽的对话地图。
统计每段对话成功完成的压缩次数并常驻悬浮窗,只有反复压缩过、且此后又烧掉大量 token 或上下文占用过高时才提醒;三个阈值都可在设置页调整。
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.
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]│
└──────────────────────────────┘
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 |
| 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").
>= 0.1.7-rc.1 (@deepseek-ai/dsh)>= 22.19web, or desktop for the Electron app (this plugin
has a browser half; it is a no-op on surfaces without one)dsh plugin --profile web add @hjdd14/dsh-toolong-warning
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.
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.
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.
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:
@hjdd14/dsh-toolong-warning from its Plugins page.<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.
npm pack # produces hjdd14-dsh-toolong-warning-0.3.0.tgz
dsh plugin --profile web add "file:<path to the tgz>"
dsh plugin --profile web remove @hjdd14/dsh-toolong-warning
Or disable it from the dsh web sidebar's Plugins page.
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.
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.
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.
A desktop shell owns the window chrome, and the floating window is aware of it:
--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.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.
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.
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)
| # | 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 |
?selftest=1 makes
the running process prove that on real sessions.$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.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.@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.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.
| 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 |
MIT © 2026 Hjdd14