dsh-message-handoff
Per-message session handoff for DeepSeek Harness.
Inspired by dsh-session-handoff, with two
differences: the trigger is a handoff icon next to each message's fork button (no slash
command), and the forked session keeps the copied history up to the selected message with the
LLM summary injected on top of it.
Each finalized assistant message's icon row renders copy · ✋ handoff · fork. Clicking handoff:
- summarizes the conversation up to that message with one auxiliary LLM call (thinking off,
byte-capped input, oldest entries dropped first);
- creates a fresh session with the copied history prefix (same workspace cwd, parent lineage,
and agent preset — the same pipeline the web UI's own fork button uses);
- appends the structured handoff document (task goal / progress / decisions / files / next steps /
open questions) as a user message on top of the copied history;
- attaches the child to the source's workspace, pins a
handoff: <title>, and auto-opens it.
The origin session stays untouched. Any summary failure (no route yet, stream error, timeout,
empty output) leaves zero side effects — no session is created.
⚠️ Privacy: the conversation text up to the selected message is sent to the configured model
to produce the summary.
Install
# no build step needed — lib/client.js is prebuilt
dsh plugin --profile web add /path/to/dsh-message-handoff
dsh web
dsh --profile web --dump-config | grep message-handoff
dsh plugin add forwards to pnpm add inside the profile directory, so any install spec works: a directory checkout like above, a git URL once pushed, or the bare package name after publishing. Updating later means editing and re-running the same command.
Zero runtime dependencies: the host half is plain ESM over public services, and the client half is
the official prebuilt browser module (lib/client.js). npm test runs the pure-function unit
suite with node --test.
Configuration
Optional config: on the cordis.yml row:
- id: message-handoff
name: dsh-message-handoff
config:
maxInputBytes: 24000 # UTF-8 byte cap for the summary input (oldest dropped first)
maxTokens: 2000 # summary output budget
timeoutMs: 120000 # summary call timeout
The summary model route is the source session's latest logged request route (the model that
actually produced the history), falling back to the deployment's default model selection.
How it works
- Host half (
index.mjs): harness.handle('message-handoff:create') — resolves the fork cut
for the addressed messageId (first completed turn/end at or after it, extended through
trailing out-of-band appends, exactly like the api-proxy fork handler), summarizes via
ctx.llm.stream, creates the child via ctx.agents.create({ seed, seedLength, meta, setup })
with agentPresets.resolve → mount, then best-effort workspaceRegistry attach and
sessionTitle.rename, and appends the handoff document with surfaceOp: 'append'.
- Client half (
lib/client.js): registers one additive entry in the
conversation.chat.assistant-actions slot (rendered between the built-in copy and fork
controls), calls the package RPC, polls sessions.binding until the child is addressable, and
opens it. A failed handoff turns the icon red with the error in its tooltip; clicking retries.
Model Experience
Summary request (auxiliary model call)
What the model sees
One auxiliary llm.stream request per handoff click: this package's system prompt below plus a single user message whose text is a JSON array of the extracted conversation turns [{"role":"user"|"assistant","text":"..."}], oldest entries dropped first until it fits maxInputBytes.
Verbatim summary system prompt
You are producing a handoff summary that will be prepended to a copied conversation prefix in a fresh agent session.
The user input is a JSON array of conversation turns: {"role":"user"|"assistant","text":"..."}.
Produce a concise structured handoff in Markdown with these sections:
## Task goal
## Current progress (what is done and what is not, concrete)
## Key decisions (and their reasons)
## Files involved (paths only)
## Next steps (numbered)
## Open questions / risks
Write in the language of the conversation. Be concrete and factual; do not invent facts that are not in the conversation.
Return only the handoff document, no preamble, no code fences.
Token effect
Conditional and capped: zero tokens until a handoff runs. The one-shot request is bounded by the maxInputBytes input framing and the maxTokens output budget; reasoning effort follows the route default unless the route's adapter lists off, which the summary then pins.
KV Cache effect
Independent behavior: the summary is a separate one-shot call outside any session loop, so it neither reuses nor invalidates provider caches for the source or child conversations.
Handoff document injection
What the model sees
In the forked child session, one appended user/message headed "📋 Session handoff": the source session id, the generated summary document, and two orientation lines pointing at "Next steps". The copied seed history precedes it unchanged.
Token effect
Append-only growth of the child's context: the copied prefix carries the same tokens as the source history up to the fork cut, plus the document, which scales with the summarized conversation length under the same maxInputBytes cap.
KV Cache effect
Append-only: the seed replays identically from persistence, so the copied prefix stays prefix-stable across the child's later turns; the injected document sits after that prefix and never rewrites earlier request tokens.
Known Limitations and Deferred Work
- Assistant-turn-only trigger — the icon renders where the shipped fork control lives, on finalized assistant turns; user messages expose no per-message action strip to sit beside.
- Preset read from the creation header — a session that switched agent presets mid-flight forks its child under the original composition instead of re-deriving the switch from the log.
- No durable artifact — the handoff document lives only in the child session log; writing
HANDOFF.md remains deferred work.
- Text-only summarization — tool calls and attachments are excluded from the summary input; only
user/message + assistant/message text blocks are extracted.
License
MIT