opencode-to-dsh
August 14, 2026 · View on GitHub
Import opencode session exports into DeepSeek Harness (DSH) as native, resumable sessions.
What it does
- Reads an
opencode export <sessionID>JSON file and writes a DSH session log (session.jsonl.zstd) that DSH can list, open, and resume. - Maps user/assistant messages, reasoning, and tool calls (call + result pairs) onto DSH's event-sourced log format.
- Handles long sessions that were compacted in opencode: it detects
compactionmarkers, imports only the post-compaction tail window, and prepends a summary assembled from the assistant's own pre-compaction handoff messages — so the imported context stays within the model's context window. - Optional five-layer validation before you install anything (
--validate), reusing DSH's own reader and validation functions.
Files
| File | Purpose |
|---|---|
opencode-to-dsh.py | The importer (standalone; only needs Python 3 + zstd CLI). |
validate.mjs | Optional quality gate — the five-layer check (needs Node.js + an installed DSH). |
Requirements
- Python 3 and the
zstdcommand-line tool (for conversion). --validateadditionally needs Node.js and an installed@deepseek-ai/dshonPATH(auto-detected; override with theDSH_PKG_ROOTenvironment variable pointing at the@deepseek-ai/dshpackage directory).
Quick start with the bundled samples
# full mode (small session)
python3 opencode-to-dsh.py examples/sample-full.json ./out --validate
# tail mode (compacted session, auto-detected)
python3 opencode-to-dsh.py examples/sample-compacted.json ./out --validate
Both samples pass the five-layer validation; inspect ./out/sessions/ for the generated logs.
Usage
# Convert only (no validation)
python3 opencode-to-dsh.py session-export.json ./out
# Convert + five-layer validation (recommended before installing)
python3 opencode-to-dsh.py session-export.json ./out --validate
Options
| Option | Description |
|---|---|
--mode full|tail|compact | Import mode. Default: tail when the export contains compaction markers, else full. |
--sid session-xxx | Fixed session ID (default: random). Useful for in-place replacement. |
--cwd /path | Override the workspace directory (default: the export's directory, then the current directory). |
--title 标题 | Override the session title (tail appends " (续聊版)", compact appends " (压缩版)"). |
--tz 时区 | Timezone recorded in user-message sources (default: auto-detected from the machine). |
--model 模型ID | Override the model identifier (default: the export's model.id). |
--provider 提供方 | Override the provider identifier (default deepseek-official). |
--validate | Run validate.mjs after conversion (must sit next to this script). |
--export-out 路径 | Also write the filtered intermediate export JSON. |
Modes
- full — imports the entire history. Suitable for small sessions or read-only archives.
- tail — for long sessions compacted in opencode. Finds the last compaction's
tail_start_id, imports only the messages after it, and inserts a synthetic first user message containing a summary composed from the assistant's pre-compaction summary texts (the last text-bearing assistant message before each compaction point, capped at 800 chars each). This replicates what the model actually saw in opencode, keeping the resume context within the context window. - compact — the full-fidelity variant: imports the entire history (fully browsable in the UI), then inserts a native DSH compaction transaction per opencode compaction point, at the position where it happened —
compaction/start→compaction/summary→ a checkpointuser/messagecarryingsurfaceOp: {op: 'replace'}→compaction/end, mirroringdsh-compaction-basic's own event choreography. Each checkpoint shadows the surface nodes live at that point, so the model's derived context is exactly[final summary checkpoint + tail]while the UI transcript keeps every message and every "context compacted" marker appears in the right place. Checkpoint summaries reuse opencode's own adjacent summary messages (## Objective …) when present, falling back to composed pre-compaction summaries; shadow prices use the same fixed estimator asdsh-token-meter, keeping the context-breakdown projection non-negative. Real-world result on a 24.7 MB export (2,861 messages, 4 compactions): 4 checkpoints, derived context 10.7 MB → 1.5 MB (13.8%).
Export format compatibility
Two opencode export formats are detected per compaction marker:
- opencode 1.17: the
compactionpart carriestail_start_id(the first message of the retained window); the boundary keeps every message from there on, matching opencode's actual retention. - opencode 1.18: no
tail_start_id; the adjacent assistant message after the marker is the real summary, and the tail starts after it.
Pure compaction-marker messages (no text content) are skipped in every mode.
Tool name mapping
opencode tool names are mapped to DSH names on import: websearch → web_search, webfetch → web_fetch, question → ask_user_question, todowrite → todo_write, task → subagent. Names already shared (read, write, edit, bash, grep, glob, …) pass through unchanged; unknown names are kept verbatim.
Installing into DSH
The output layout mirrors DSH's own storage:
<out>/sessions/<projectKey>/<session-id>/session.jsonl.zstd
<projectKey> is derived from the session's cwd (e.g. /home/user/project → --home-user-project--).
# Example: install for the current workspace
mkdir -p ~/.dsh/sessions/--home-user-project--/<session-id>
cp <out>/sessions/--home-user-project--/<session-id>/session.jsonl.zstd \
~/.dsh/sessions/--home-user-project--/<session-id>/
Then restart dsh web (or refresh the browser) and open the session. Sessions land under the workspace matching their cwd; switch workspaces in the sidebar if needed.
The five validation layers (validate.mjs)
| Layer | What it checks |
|---|---|
① loadStored | zstd decoding, header shape, seq continuity — via DSH's own JsonlSessionPersistence |
② adoptSessionEvent | message shape (id/role/source/content) — DSH's own validation function, per event |
③ foldSurface | surfaceOp markers and surface folding — DSH's own function |
| ④ inbox replay | agent/inbox/spliced boundaries and duplicate pending ids (mirrors Inbox.apply) |
| ⑤ derived-history pairing | every tool/result must follow an assistant message carrying a matching tool-call (the API's requirement) |
Layers ①–③ call the real DSH functions from your installed packages, so a pass here means the real load path will accept the log.
Troubleshooting
| Symptom | Cause |
|---|---|
lacks an identified message | A message event is missing id/role (this importer emits them). |
requires a surfaceOp marker | A message event is missing its surfaceOp (this importer emits them). |
invalid persisted inbox splice | Inbox events that remove messages never inserted (this importer emits append + claim pairs). |
Messages with role 'tool' must... | Tool results without a preceding assistant tool_calls (this importer embeds tool-call blocks). |
maximum context length | Importing a huge session in full mode; use tail mode instead. |
无法定位 DSH 安装 | dsh not on PATH and DSH_PKG_ROOT not set (validation only). |
Limitations
- DSH session format is version 0 with no compatibility promise; an importer output may become unreadable after a DSH upgrade. Re-run the importer against a fresh export if needed.
- Tool cards are viewable but not re-executable: opencode tool outputs are imported as static results.
- Resume is supported, but a resumed conversation appends new events to the imported log — the summary/tail structure is preserved.
- Unknown opencode part types (e.g.
patch,snapshot) are skipped; they carry no model-visible content.
Privacy
- No credentials, API keys, or config are read or written.
- Paths come from the export itself; nothing machine-specific is hardcoded (the workspace fallback is the current directory, and the timezone is auto-detected, both overridable).
Acknowledgment
This project was developed in an agent session of DeepSeek Harness (DSH), powered by DeepSeek models, with requirements and design led by a human user. It bridges opencode session exports into DSH; both opencode and DeepSeek Harness are MIT-licensed open-source projects.