Skip to content
dsh.fish
Bundle

dsh-message-handoff

Per-message session handoff for DeepSeek Harness: a handoff icon next to each message's fork button summarizes the conversation up to that message with an LLM, forks a fresh session carrying the copied history plus the injected summary document, and auto-opens it. The origin session is unchanged.

Source
antonkoatl
stars
1 stars
License
MIT
Updated
Updated 13 days ago

Readme

# dsh-message-handoff

Per-message session handoff for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
Inspired by [`dsh-session-handoff`](https://github.com/WeiYe6/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:

1. 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);
2. 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;
3. 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;
4. 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

```sh
# 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:

```yaml
- 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 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'`. The success text names the child id in backticks.
- **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), dispatches `/message-handoff` through Remote `commands.execute`, extracts the child
  id from the result, polls `sessions.binding` until 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

```markdown
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.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](./LICENSE)

Install

dsh plugin --profile web add github:antonkoatl/dsh-message-handoff

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source