dsh-migrate
English | 中文
Import chat history and memory from other AI agents into DeepSeek Harness.
Switching harnesses should not mean losing your context. dsh-migrate reads the transcripts and memory stores other agents leave on disk and writes them as native DeepSeek Harness session logs — searchable, inspectable in the Trajectory view, and resumable (--fork-style continue) as if they had been recorded by DSH itself.
Supported sources
| Agent | Sessions | Memory |
|---|---|---|
| Claude Code | ~/.claude/projects/**.jsonl (text, thinking, tool_use/tool_result) |
~/.claude/CLAUDE.md, project memory/*.md |
| Codex CLI | ~/.codex/sessions/**/rollout-*.jsonl (messages, reasoning, function calls) |
~/.codex/AGENTS.md, instructions.md |
| Gemini CLI | ~/.gemini/tmp/**/chats/session-*.json (messages, thoughts, toolCalls) |
~/.gemini/GEMINI.md |
| Cline / Roo Code | VS Code globalStorage/*/tasks/*/api_conversation_history.json |
— |
| OpenCode | ~/.local/share/opencode/** message/part JSON files |
— |
| Aider | **/.aider.chat.history.md (bounded scan) |
CONVENTIONS.md files |
| Hermes Agent | ~/.hermes/state.db (SQLite sessions+messages, read-only) |
~/.hermes/memories/MEMORY.md, USER.md |
| OpenClaw | ~/.openclaw/agents/*/agent/openclaw-agent.sqlite (transcript_events, read-only) |
workspace MEMORY.md, memory/*.md |
Every adapter is tolerant: malformed lines and unknown row types are skipped, never fatal. Missing fields degrade gracefully (unknown model → imported, unknown cwd → the _no-cwd project).
Install
# 1. Install the plugin into your DSH web profile
dsh plugin --profile web add @ersss/dsh-migrate
# 2. Restart DSH
dsh web
The package declares a dsh.bundle patch layer, so installing it does two things at once:
- registers the
/migrateslash command, and - points DSH's session store at
$DSH_HOME/sessions-importedin raw-JSONL mode — a dedicated root for imported logs that never mixes with the default store's encoding.
Heads-up: the bundled patch relocates the default session root, which means sessions you recorded before installing the plugin stay in the old
sessions/root and stop being listed. If you have existing DSH sessions you still want listed, either merge the patch's two rows into$DSH_HOME/profiles/web/cordis.patch.ymland keep both roots by alternating--patch, or simply run the CLI with--outpointed at your live root (raw JSONL reads fine under a zstd root only if the root was created withcompression: 'none'). Fresh installs are unaffected.
Use
From inside DSH (Web UI, TUI, headless):
/migrate list # show what each agent has on disk
/migrate run # import everything
/migrate run --source claude-code --limit 10
From a terminal, without DSH running (also the right choice for big archives):
npx @ersss/dsh-migrate list
npx @ersss/dsh-migrate run # → $DSH_HOME/sessions-imported
npx @ersss/dsh-migrate run --out /path/to/root # anywhere you like
npx @ersss/dsh-migrate run --write-instructions . # also fold memory into ./AGENTS.md
Imported sessions appear in the sidebar grouped by their original project directory, and in the Trajectory view with their full event stream — prompts, reasoning, tool calls, and results.
What gets written
DSH's session log is an append-only JSONL event stream (SESSION_FORMAT_VERSION 0). For every source session, dsh-migrate writes one log:
$DSH_HOME/sessions-imported/--<project>--/<id>/session.jsonl
{"type":"session","version":0,"id":"…","createdAt":…,"delegationDepth":0,"cwd":"…"}
{"type":"turn/start","seq":0,"time":…,"data":{"turn":1}}
{"type":"user/message","seq":1,…,"data":{"role":"user","source":{"kind":"user","via":"dsh-migrate:claude-code"},"content":[{"type":"text","text":"…"}]},"surfaceOp":"append"}
{"type":"step/start",…} {"type":"assistant/message",…} {"type":"tool/call",…} {"type":"tool/result",…} {"type":"step/end",…}
{"type":"turn/end",…,"data":{"turn":1,"reason":{"kind":"completed"}}}
…
{"type":"session/end-seed",…} ← marks the log as imported seed history
Mapping decisions, briefly:
- Turns. One source user prompt (plus the assistant work it triggered) becomes one
turn/start…turn/endbracket. Tool-result-only user rows (Claude Code, Cline) attach to the pendingtool/callinstead of opening a turn. - Messages. User and system text become
user/messageevents; assistant text/reasoning/tool-calls become oneassistant/messageper step; tool results becometool/resultpaired by call id. - Provenance, not impersonation. Imported assistant messages keep the original provider and model (
provider: "dsh-migrate:anthropic", model: "claude-fable-5"). Imported user messages arekind: "user"— required for the trajectory view and history derivation — with the importer declared on the merge-extensibleviachannel (via: "dsh-migrate:claude-code"). - Timestamps and ids. Original timestamps are preserved when the source records them (monotonicity enforced); missing call ids get fresh UUIDs; session ids are fresh UUIDs (never the source's) so an import can never shadow a live session.
- Memory. Each source's memory records become one "Imported memory" session (readable, searchable, resumable), and
--write-instructions <project>additionally folds them into that project'sAGENTS.mdbetween<!-- dsh-migrate … -->markers (idempotent re-runs). - Append-only honesty. Writes use
wx(no overwrite). A colliding id is retried with a fresh one; existing logs are never modified.
How resume works
The trailing session/end-seed event makes the whole imported log seed history: when you continue an imported session, DSH replays the log as inherited context, then starts a clean live segment — the model sees the prior conversation exactly as DSH would have logged it. The agent preset and tools of the current runtime apply; tools the old agent had and DSH does not simply become history.
Notes & limitations
- Read-only on sources. The importer never writes to the source agents' directories.
- Images and attachments are not imported (DSH attachments are content-addressed host objects); their surrounding text is.
- Compaction history in source transcripts is flattened to plain messages.
- SQLite sources are read immutably (
mode=ro&immutable=1), so a running Hermes/OpenClaw gateway is never disturbed. OpenClaw imports everysession_window; resets/rollover windows appear as separate sessions (they were separate transcripts upstream too). - Aider scan roots are
$DSH_MIGRATE_SCAN(path-list),~/projects,~/code,~/dev,~/work, and a shallow$HOMEsweep — its history files live inside projects, so a fixed home-relative path cannot find them. - Format drift: these layouts are undocumented internals of the source agents and change over time. Adapters skip what they do not recognise; if your agent version is newer, please open an issue with a (redacted) sample row.
Development
npm install
npm run build # tsc → dist/
npm test # fixture-driven end-to-end tests (real DSH codec replay)
The test suite replays produced logs through the actual @deepseek-ai/dsh-session codec (decodeStorageRecord + foldSurface) vendored from a live DSH profile, so format regressions fail loudly.
Contributing a source adapter
One adapter = one file in src/formats/ implementing:
interface SourceAdapter {
key: string
label: string
detect(home: string): Promise<DiscoveredSource | undefined>
readSessions(home: string, options?: { limit?: number }): Promise<IrSession[]>
readMemories(home: string): Promise<IrMemory[]>
}
Register it in src/formats/index.ts and add a fixture test. See src/formats/claude-code.ts for the reference implementation.
License
MIT
No comments yet. Be the first to write one.