dsh-undo
No description
- Stars
- 1
- Language
- JavaScript
- Created
- Aug 24, 2026
- Updated
- Aug 24, 2026
Introduction
dsh-undo
Undo button and side ruler for DSH conversations: rewind to any earlier prompt and re-edit it via a fork.
Built on the Cordis reversibility paradigm — the session log is the context, and undoing a prompt recovers the context to an earlier prefix (a withdrawal) without ever mutating the original log.
Features
- ↶ Undo button in the session header. Hovering opens a dropdown with the trajectory of your previous prompts (one entry per user turn, most recent first). Picking one forks the session at the turn boundary right before that prompt, opens the fork, and prefills the composer with the prompt text so you can edit it and resend.
- Side ruler under the Chat tab (replaces the shipped QueryRail while this plugin is active):
- one tick per user/steering message, always visible (the shipped rail needs at least two prompts);
- left-click jumps the conversation to that prompt;
- right-click shows a confirmation and then performs the same undo directly from the ruler.
- The active tick follows your scroll position.
Install
From the plugin market entry, or directly:
dsh plugin --profile web add github:tofe98/dsh-undo
No host half, no build step, no RPC: everything is computed from the live conversation snapshot.
Usage
- Send a couple of prompts in a session.
- Hover ↶ in the session header → pick an earlier prompt → a forked child session opens, rewound to just before that prompt, composer prefilled with the prompt text.
- Edit and resend, or right-click a ruler tick → Undo (after confirmation) for the same flow from the side ruler.
Notes:
- The original session is left untouched — undo is a fork (the paper's withdrawal semantics: recovery without destroying the log).
- The child conversation is rebuilt through DSH's projection machinery (the harness's cached-context path).
- Undoing restores the conversation state, but external side effects of the undone turns (file edits, web calls) are not reverted — they are emissions outside the system boundary (§6.1 of the paper).
How it works
- The browser half reads the live conversation snapshot: user/steering nodes for the trajectory and the turn-end map (
chat.turnEnds) to compute each prompt's fork boundary (theturn/endseq of the previous turn). - Undo calls
sessions.fork({ sessionId, atSeq: boundary }), opens the child, and prefills viainputActions.setDraft(text). - UI lives in
conversation.session.header.actions(button) andshell.overlay(dropdown panel + side ruler).
Cache hit: the undo + resend test
Undoing and resending the same prompt preserves the whole request prefix, so provider-side
prefix caching should reuse it. We measured it end to end (DeepSeek automatic prefix caching,
deepseek-v4-flash, reasoning effort High):
| run | raw usage (assistant/chunk) | per-request hit |
|---|---|---|
| msg 1 "ciao, mi piace deepseek" | input=8107, cacheRead=1792 | ~22% (shared system-prompt prefix only) |
| msg 2 "molto interessante, vorrei parlare con te" | input=134, cacheRead=9856 | 98.7% |
| undo msg 2 → resend identical | input=6, cacheRead=9984 | 99.94% |
The resend read 9 984 of 9 990 billed input tokens from cache — the undo+resend flow is effectively a full cache hit. The ~6 residual tokens are the reserialized tail of the new user message (it gets a fresh id in the fork).
Why the conversation's "Cache hit: 59%" line still showed ~59% in both runs:
- That readout is cumulative over the session, not per request: it sums every usage chunk
in the log, including the first request's large cold miss (the runtime-context snapshot,
the skill-catalog system reminder, and the prompt itself are new). Both sessions end at
(1792+9856)/(8107+134+1792+9856) ≈ 59%because the fork inherits the parent's msg-1 usage chunk and shares the same total. - The per-request number lives in the session log:
assistant/chunkevents with{"type":"usage","usage":{...}}, wherehit = cacheReadTokens / (cacheReadTokens + inputTokens). The Trajectory view also shows per-cellcacheRead. - Note the adapter's
inputTokensfield is not uniform (msg 1 reports the total, later chunks the new-only tokens), so the UI percentage can understate the real hit — the raw chunks are the ground truth.
Takeaways for cache-hit optimization:
- Undo (fork at the previous turn boundary) preserves the entire message prefix — the ideal flow for prefix caching.
- Keep the model fixed and the system prompt deterministic (no timestamps in this deployment), and re-run within the provider's cache validity window; after long idle the first re-run misses and the second hits.
- The ~18–22% floor on the first request comes from the shared system-prompt prefix cached across sessions; a session whose first request is already warm shows ~100% from the start.
Structure
lib/
index.js # node half — empty apply so the Loader has a host-side row
client.js # browser half — the whole feature (module-loader format)
cordis.patch.yml
package.json # dsh.bundle + dsh.client declarations
Development
The plugin is plain JavaScript — no build step. To try it during development, either load lib/client.js through the harness's dynamic plugin system or install the package from a local path.
License
MIT