Sessions & Messages
dsh-rewind
XSJUSTC/dsh-rewind · ★ 12
DSH (DeepSeek Harness) conversation rewind plugin - rewind to any user message, interrupt a running turn, hide the tail from chat view and model context, cancel freely before sending.
Manifest details
安装
dsh plugin --profile web add github:XSJUSTC/dsh-rewindGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
English | 简体中文
A conversation-rewind plugin for DSH (DeepSeek Harness). A permanent bundle plugin with a host half and a web client half — zero dependencies, zero build steps.
Features
- Every user message row gains a ↺ rewind action beside the copy icon.
Clicking it:
- interrupts the current turn if the model is still thinking/streaming;
- hides that message and everything after it from the chat view (as if the tail never happened);
- pre-fills the composer with the message text — unsent and editable;
- re-attaches any images that rode the message back into the composer (the host reads the original bytes from the session log and feeds them back through the official attachment intake, so image count/size limits apply);
- on the next send, the model only sees the truncated history (everything before the rewind point) plus your new message.
- While a rewind is pending:
- a banner above the composer explains the state;
- a ✕ "cancel rewind" button sits left of the send button — clicking it restores the hidden messages, keeps your draft, and sends nothing.
- Once sent (rewind committed), the hidden tail never reappears in the chat view and never enters the model context again.
- Only user messages can be rewound; assistant messages have no rewind entry (the host validates the event type).
Version compatibility
- v2.4.0 is verified against dsh 0.1.5-rc.2 while keeping earlier 0.1.x
hosts working — event-log access prefers the official
snapshotEvents()/eventAt()and falls back tosession.eventson older hosts. - v2.4.0 fixes "the model's output was not hidden" (the main fix here):
- Cause: the plugin shadows only the
user/steeringrenderers ofconversation.chat.node, so only those rows ever carried adata-xsj-seqstamp. The model'sassistant-stepoutput, tool calls and context rows render through the shipped renderer and were never stamped. The hiding pass treated an unstamped row as "no evidence" and therefore visible, so a committed range hid the prompt while the answer stayed on screen. - Fix: an unstamped row is no longer given up on. It inherits the verdict
of the nearest stamped row before it, which is the turn it belongs to;
only a leading run with no evidence at all falls back to the evidence that
follows. The forward attribution matters: "hidden if anything later is
hidden" sweeping in the answer above a range is a real regression that was
hit during development and is now covered by
test/hiding.test.mjs. - Restore the DOM on driver teardown: hiding writes to the live DOM, but
teardown only cleared the timer and observer. Switching sessions or
remounting the composer left the hidden rows for the next view (blank rows
out of nowhere). Teardown now releases every row carrying this plugin's
marker — and only those, leaving any other feature's
displayuntouched. - Tell "no data yet" apart from "no ranges": on a cold load the driver ran
before
/stateanswered, and the two were indistinguishable, so every load flashed the rewound tail back into view; if the read failed it stayed visible for good, leaving the UI showing messages the model cannot see. Sessions now trackfetched/fetchFailed: while no answer has arrived the current verdicts are held and a retry is scheduled, and a locally recorded mark still applies immediately. - Host:
agent/disposedno longer strands the patch:ensurePatchedinstalls aderiveMessagesoverride on the session instance closing overst.ranges, and disposal used to drop the state record outright, leaving that override with no record to unwind it — the session kept filtering its model context permanently, surviving a plugin reload. Disposal now follows the same rule asevictIfNeeded: unwind first, then drop. ensurePatchedis self-healing: it no longer trusts thest.patchedflag alone but compares the installed method against the recorded wrapper and re-installs when they diverge (idempotent), so a flag that drifts out of sync cannot silently stop hiding the tail from the model.- Image references cache hits only: a reference, once in the append-only
log, can never disappear, so a hit stays valid; a miss cannot, and caching
one made
/imageanswer 404 for an id the log does reference — surfaced as an image silently dropped on rewind.
- Cause: the plugin shadows only the
- Changes in v2.3.0:
- Rewind now re-attaches images: the host half exposes
/api/xsj-rewind/imageto read a message's durable image bytes by attachmentId, and the client rebuildsFileobjects and feeds them back through the composer's hidden file input, inheriting the shipped image count/size validation. - Fixed a cascading render crash on image messages: the client half of
@deepseek-ai/dsh-client-ui-attachmentexports onlyapply/injectin 0.1.5-rc.2 (noImageGallerycomponent), so referencing it threw inside any image-bearing user row and the React error boundary widened the damage to the surrounding rows — the rewind button vanished across a whole span of messages. Images now render through the shippedrenderMessageImagesprop (routed to theconversation.message.imagesslot). - Hardened slot registration: the
slotsservice is resolved defensively and each seat registers under its owntry/catch. A single registration conflict (e.g. a client HMR rebuild race) previously abortedapply(), leaving the stylesheet injected with every renderer lost. - Safer DOM hide driver: an unstamped row no longer inherits a neighbour's hidden verdict, so a freshly sent message cannot be hidden while its view is still stamping. (Corrected in v2.4.0: that same conservatism made the model's output permanently un-hideable — see above.)
- Serialized image re-attachment so concurrent rewinds cannot race on the same file input.
- The host half resolves
attachmentsat call time (ctx.get), so a missing attachment provider no longer blocks the mount; the image endpoint alone degrades to a 501. - Resource bounds: the per-session state table and the image-reference cache are capped, and image responses plus attachmentId length are validated.
- Rewind now re-attaches images: the host half exposes
- Changes in v2.2.0:
- Adapted to 0.1.5-rc.2:
primitives.MessageTextwas removed; the user bubble now renders text through the sameprojectUserText()helper the shipped bubble uses (reference/session chips included); - Client inject declaration updated: dropped the retired
@deepseek-ai/dsh-client-runtime; - Dead code and debug scaffolding removed (debug log file, fuzzy session resolution, two unused code paths).
- Adapted to 0.1.5-rc.2:
- Known symptoms of ≤2.1.2 on dsh 0.1.5-rc.2: user message rows crash while rendering, no rewind button — upgrade to ≥2.3.0.
Security notes
- The four endpoints (
mark/cancel/state/image) register on the dsh web server and therefore follow dsh's own local-trust model:dsh weblistens on127.0.0.1by default, so they are reachable from this machine only and do not pass through dsh's API authentication layer. If you bind dsh web to a routable address (e.g.0.0.0.0), these endpoints become reachable too — restrict them at the network layer (firewall or an authenticating reverse proxy) in that case. - The
/imageendpoint is scoped by the session log: it returns bytes only for attachments that session actually references (matched by attachmentId against the event stream). There is no path-based or arbitrary-id read. The bytes come from dsh'sattachments.readImage, which verifies integrity. - Limits: 64 KB request bodies, 256-character attachment ids, 64 MB per image response.
- The plugin collects and reports nothing; all state stays in the local session log.
Log & recovery
- The session log (an append-only event stream) never loses a message.
- Every state change appends a
hook/invokedevent with payload{ source: 'xsj.rewind', phase: 'mark' | 'cancel' | 'commit', targetSeq, hiddenFrom?, hiddenTo?, preview? }. The type is a known-but-unused reserved entry in this build, so the records are reload-safe. - After a process restart, opening a session replays these records to rebuild the hidden ranges — model-side and UI-side hiding stay consistent across restarts.
- The trajectory view does not render this reserved event type; audit the raw session JSONL log directly.
Install
# from anywhere; the path points at your clone of this repo:
dsh plugin --profile web add <absolute path to this repo>
# restart the DSH process, then refresh the browser page
The command registers the package into the profile
(~/.dsh/profiles/<name>/package.json: dependencies + dsh.profile.bundles).
At boot the profile merges this package's cordis.patch.yml, inserting the
host plugin row; the web client scan then serves lib/client.js automatically.
Uninstall
dsh plugin --profile web remove @xsj/dsh-rewind
# restart DSH. Rewind records in session logs are harmless (hook/invoked is a
# known event type).
How it works
- Model-side truncation: patches the live
Sessionobject'sderiveMessages()to skip hidden surface ranges (memoized by a signature of surface size / range count / replace generation). The request builder, thellm/streamreconstruction invariant, and the image check all share this one method, so every reader stays consistent. No message event is ever added, removed, or rewritten. - Interrupt: on mark, a running agent is cancelled with
agent.cancel({ kind: 'user' }, { keepInbox: true }); queued messages survive and continue from the rewind point. - Commit point: inside the
agent/pre-stepwaterfall, the first step that claims a real input message seals the hidden range[targetSeq, current log end]; the new message is appended afterwards and stays visible. - UI hiding: purely DOM-driven — chat rows carry
data-chat-flow-key; the client stamps each row with its seq and a MutationObserver applies inlinedisplay:noneto hidden rows. Nothing depends on the host store's internal shape; cancelling or switching sessions restores everything without touching any shipped renderer. - Rewind icon: takes over the
user/steeringcells ofconversation.chat.nodeat priority-1(the slot system's native shadowing), replicating the native bubble (projectUserText / ImageGallery / Tooltip / writeClipboard) plus the ↺ button.
Porting
The plugin has no npm dependencies: the host half uses only Node builtins and
injected services; the client half pulls react,
@deepseek-ai/dsh-client-ui-primitives, and
@deepseek-ai/dsh-client-ui-attachment from the shell's frozen module table
via window.__ModuleLoader__. Clone this repo on any machine and follow
"Install".