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 compaction markers, 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

FilePurpose
opencode-to-dsh.pyThe importer (standalone; only needs Python 3 + zstd CLI).
validate.mjsOptional quality gate — the five-layer check (needs Node.js + an installed DSH).

Requirements

  • Python 3 and the zstd command-line tool (for conversion).
  • --validate additionally needs Node.js and an installed @deepseek-ai/dsh on PATH (auto-detected; override with the DSH_PKG_ROOT environment variable pointing at the @deepseek-ai/dsh package 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

OptionDescription
--mode full|tail|compactImport mode. Default: tail when the export contains compaction markers, else full.
--sid session-xxxFixed session ID (default: random). Useful for in-place replacement.
--cwd /pathOverride 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 模型IDOverride the model identifier (default: the export's model.id).
--provider 提供方Override the provider identifier (default deepseek-official).
--validateRun 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 happenedcompaction/startcompaction/summary → a checkpoint user/message carrying surfaceOp: {op: 'replace'}compaction/end, mirroring dsh-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 as dsh-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 compaction part carries tail_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)

LayerWhat it checks
loadStoredzstd decoding, header shape, seq continuity — via DSH's own JsonlSessionPersistence
adoptSessionEventmessage shape (id/role/source/content) — DSH's own validation function, per event
foldSurfacesurfaceOp markers and surface folding — DSH's own function
④ inbox replayagent/inbox/spliced boundaries and duplicate pending ids (mirrors Inbox.apply)
⑤ derived-history pairingevery 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

SymptomCause
lacks an identified messageA message event is missing id/role (this importer emits them).
requires a surfaceOp markerA message event is missing its surfaceOp (this importer emits them).
invalid persisted inbox spliceInbox 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 lengthImporting 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.