dsh-longsession
A browser-side plugin for DeepSeek Harness Desktop / Web that keeps long chat sessions from getting slower as the transcript grows.
Repository: https://github.com/asots/dsh-longsession
The host half is an inert Loader row; all behaviour lives in lib/client.js. It puts a
“Compact page” toggle in the session header row and bounds the history already rendered by
the page.
The measured problem
DSH never unloads history the page has already rendered: live appends plus every “load earlier” keep accumulating DOM rows. In a 4.4 MB / 11-turn session on a clean test host:
- opening the session: 892 ms to ready, one batch render, 808 ms max long task, DOM 1,240 → 9,218 nodes, heap 36.6 → 57.6 MB;
- each “load earlier”: one 300–630 ms main-thread task; rows 368 → 1,330, DOM 9k → 30k, heap 58 → 121 MB;
- steady scrolling at 775 rows: 60 fps, zero long tasks — so scrolling/paint is not the bottleneck, and CSS containment measured neutral.
What the plugin does
- Compact page — calls the app’s own
SessionFace.resync()through thesessionsclient service. This unloads the history rows the page has already rendered and reopens the ordinary opening page (last two turns). Chat records, model context and the disk log are not deleted. - Toggle — while the page is not compacted the button reads “Compact page”. After a
compaction it reads “Page compacted”; clicking it again restores the window that was loaded
before compaction. A restore also turns auto-compaction off for that session, so it cannot undo
what you just restored. The saved window (turn count and scroll offset) is stored per session in
sessionStorage; restoring asks the controller for the missing pages again throughloadOlder(). The button reads “Restoring…” while that runs. - Session-header toggle — registered in the app’s own
conversation.session.header.actionsrow (next to agent team / mode). It appears oncetrimMinRowsrows are loaded, but stays disabled until older history is loaded (the opening page has only two turns). A status dot sits before the label: amber = older history is loaded and can be compacted; green = already compacted; gray = not actionable yet (no older history, a turn is running, or restoring). The dot uses high-contrast light/dark colors and follows the app theme. It is a host-owned slot, not an overlay. - Settings page — Settings → Long session shows the current page load (rows and approximate turns, plus whether it is compacted) and edits the button threshold, auto-compaction threshold, request caps and whether the header toggle is shown.
- Auto-compaction — when the reader is at the live tail, no turn is running, older history
is loaded and the window exceeds
autoTrimRowsrows, it compacts once (15 s cooldown) and marks the page compacted, so it can still be restored. - Optional request cap — lowers
maxMessages/turnWindow.minTurnsin the history request before it is sent (WebSocketsession/followopen frame and unary/apipage RPC). Default only capsmaxMessagesat 200;minTurnsis opt-in.
It never moves, removes, or re-creates transcript nodes; the header toggle is the only DOM it adds.

Illustration with demo data — not a real session.

Settings → Long session.
Install
dsh plugin --profile <profile> add github:asots/dsh-longsession
Use the profile the host actually runs (for example web, or the profile a desktop build
uses). Reload the page after installing: the client bundle is loaded per page.
Manual install into a profile:
- install it as a profile dependency — e.g.
dsh plugin --profile <profile> add link:/absolute/path/to/dsh-longsession, or add"dsh-longsession": "link:..."to the profile package.json and runpnpm installthere. A bare copy undernode_modules/is not enough: the desktop plugin manager quarantines packages that are not listed as dependencies on its next install. - add the Loader row that ships in
cordis.patch.ymlto that profile'scordis.patch.yml; - reload the page (a host restart is needed only if the profile does not hot-reload patches).
Configuration
Loader-row config does not reach the browser half in this build, so override through
localStorage["dsh-longsession.config"] (JSON) or
window.__DSH_LONGSESSION__.setConfig({...}), or use Settings → Long session.
| key | default | meaning |
|---|---|---|
enabled |
true |
master switch (disabling stops auto-compaction and wire caps) |
trimMinRows |
120 |
row count at which the header toggle appears |
autoTrimRows |
600 |
auto-compaction threshold (0 disables it) |
historyMaxMessages |
200 |
cap for history maxMessages (0 = off) |
historyMinTurns |
0 |
cap for turnWindow.minTurns (0 = off; 1 halves the opening page) |
showDock |
true |
show the header toggle |
Test
node test/serve.mjs 17772
Open http://127.0.0.1:17772/harness.html; the page runs 36 assertions (wire clamp,
compact/resync, restore via loadOlder(), auto-compaction, header and settings slot
registration, subscriber updates, dispose) and prints ALL PASS.
window.__TEST_RESULTS__ is machine-readable.
Known limits
- Compacting restores the last two turns. A reader deep in old history loses the current scroll position; the button is disabled while a turn runs.
- “Page compacted” is not instant: the old pages are re-fetched from the host through
loadOlder(), one page at a time. The saved scroll offset is applied afterwards on a best-effort basis; exact pixel restoration is not guaranteed. - The toggle is visible in long pages but disabled until older history is loaded. A freshly opened session is already at the last two turns, so use “load earlier” first.
- Auto-compaction only runs at the live tail; a reader parked above the tail is never surprised.
- An explicit restore turns automatic compaction off for the rest of that session; a new session starts with automatic compaction on again.
- Compaction is per session: only the currently open session has a rendered page history to drop.
- If the
sessionsservice is absent, the header toggle stays inert and auto-compaction is disabled. If theslotsservice is absent, no toggle renders (the request caps still work). content-visibilitycontainment was measured neutral for this renderer and is intentionally not used.
License
MIT — see LICENSE.
No comments yet. Be the first to write one.