dsh-message-handoff
Per-message session handoff for DeepSeek Harness.
Inspired by dsh-session-handoff. The trigger is a
handoff icon next to each message's fork button (no slash command): forking back to an earlier
point while staying informed about later progress — the child carries the copied history up to the
selected message, and an LLM summary of everything that happened after it is injected on top.
Each finalized assistant message's icon row renders copy · ✋ handoff · fork. Clicking handoff:
- copies the verbatim history up to that message into a fresh session (its whole completed turn — the same pipeline the web UI's own fork button uses);
- summarizes only what happened in the source session after that point with one auxiliary LLM call (byte-capped input, oldest entries dropped first); clicking the latest message skips the call entirely;
- appends the structured handoff document (task goal / progress / decisions / files / next steps / open questions) covering that post-fork activity 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 AFTER 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.
lib/client.js is intentionally committed: dsh plugin installs run no registry-side build, so
the checked-in module IS the shipped artifact and directory installs work with no toolchain. It
is hand-maintained source (not generated output) — edit it directly; there is no separate build
step to drift from.
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: 10000 # summary output budget (reasoning-heavy routes need headroom)
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): registers/message-handoff <assistant-message-id>in the public command registry — resolves the fork cut for the addressed message (first completedturn/endat or after it, extended through trailing out-of-band appends, exactly like the api-proxy fork handler), summarizes viactx.llm.stream, creates the child viactx.agents.create({ seed, seedLength, meta, setup })withagentPresets.resolve→mount, then best-effortworkspaceRegistryattach andsessionTitle.rename, and appends the handoff document withsurfaceOp: 'append'. The success text names the child id in backticks. - Client half (
lib/client.js): registers one additive entry in theconversation.chat.assistant-actionsslot (rendered between the built-in copy and fork controls), dispatches/message-handoffthrough Remotecommands.execute, extracts the child id from the result, pollssessions.bindinguntil it 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 with post-fork activity: this package's system prompt below plus a single user message whose text is a JSON array of the conversation turns AFTER the fork cut [{"role":"user"|"assistant","text":"..."}], oldest entries dropped first until it fits maxInputBytes. Clicking the latest message runs no request.
Verbatim summary system prompt
You are producing a handoff summary that gives a forked agent session the context it is missing.
The transcript up to the chosen fork point was copied into the fresh session verbatim; your summary covers ONLY what happened in the source session AFTER that point, so the forked agent knows the later progress without carrying that later transcript.
The user input is a JSON array of the post-fork 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, an orientation line stating that the summary covers only post-fork activity, the generated summary, and a continuation note. 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 post-fork 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.mdremains deferred work. - Text-only summarization — tool calls and attachments are excluded from the summary input; only
user/message+assistant/messagetext blocks are extracted.
No comments yet. Be the first to write one.