Bundle
dsh-opencode-session-sync
DeepSeek Harness 插件:把 OpenCode 桌面端(opencode.db)的历史会话按各自原本的工作目录导入为 DSH 原生会话
- Source
- forwardzz
- License
- MIT
- Updated
- Updated 7 days ago
Readme
<div align="center">
# DSH OpenCode Session Sync
**Bring OpenCode desktop chat history into DeepSeek Harness as native sessions — filed under the workspace each session came from, and kept up to date as those sessions grow.**
[简体中文](README.zh-CN.md) · [Installation](#installation) · [Usage](#usage) · [Configuration](#configuration) · [Troubleshooting](#troubleshooting) · [Changelog](CHANGELOG.md) · [MIT](LICENSE)
[](LICENSE)
[](https://github.com/forwardzz/dsh-opencode-session-sync/actions/workflows/ci.yml)
[](https://github.com/deepseek-ai/deepseek-harness)
[](https://nodejs.org/)
</div>
> DSH OpenCode Session Sync is a community-maintained DeepSeek Harness plugin, not an official DeepSeek AI product. It reads the OpenCode desktop database (read-only) and writes sessions into DSH through DSH's own session persistence, so imported sessions are native: they land in the workspace matching their original working directory, and they can be reopened and continued.
## Features
- **Read-only on the OpenCode side** — opens `opencode.db` to read and never writes it.
- **Native sessions, not a side file** — writes through DSH's own `sessionPersistence` (`create` → `append` → `flush`), so DSH validates the format and the cross-event relationships. The plugin never assembles log bytes itself.
- **Filed under the right workspace** — a session's original directory becomes its `cwd`, which is how DSH files sessions; the matching workspace is registered automatically, and created if that directory has no workspace yet.
- **Incremental sync** — after the first import, turns you add in OpenCode later are appended to the same DSH session, with `turn` and `seq` continuing the existing log. `dryRun` shows what a sync would do without writing anything.
- **Idempotent** — the DSH session id is derived from the OpenCode session id, and the log itself records which source rows are already in it, so re-running never duplicates. A deleted ledger does not cause duplicates either.
- **Refuses unsafe appends** — if the source session was rewound or rewritten, the plugin reports `diverged` with the reason instead of appending a garbled transcript.
- **Automatic or on demand** — syncs shortly after DSH starts, and exposes an agent tool (`opencode_sync`) with `list`, `import` and `status`.
- **Keeps what matters** — title, creation time, model and provider, token usage, reasoning blocks, and tool calls with their outputs and errors.
- **Zero runtime dependencies** — Node built-ins only. The OpenCode database is read with `node:sqlite`, the same module DSH uses for its own session index.
## Prerequisites
- DeepSeek Harness installed and starting normally, with a host that provides the `tools` and `sessionPersistence` services. `workspaceRegistry` and `sessionProjectionCache` are used when present and skipped when not.
- Node.js 22.5 or newer, for `node:sqlite`.
- OpenCode desktop on the same machine, with its local database (default paths under [Configuration](#configuration)).
- Verified against DSH `0.2.0-rc.2` on Windows.
## Installation
### Ask an agent to install it (recommended)
Send this to an agent that can run commands on your machine. Replace `desktop` with your profile name.
```text
Install the DeepSeek Harness plugin from https://github.com/forwardzz/dsh-opencode-session-sync into my desktop profile:
1. clone it into a directory of your choice;
2. in ~/.dsh/profiles/desktop/package.json add a link: dependency pointing at that directory and append the package name to dsh.profile.bundles;
3. create the link at ~/.dsh/profiles/desktop/node_modules/dsh-opencode-session-sync pointing at the clone;
4. run `node tools/verify-install.mjs` in the clone and tell me whether it reports success.
Do not restart DSH yourself — I will do that.
```
### Install from a clone
Windows (a directory junction needs no admin rights):
```powershell
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
git clone https://github.com/forwardzz/dsh-opencode-session-sync.git "$env:USERPROFILE\.dsh\plugins\dsh-opencode-session-sync"
New-Item -ItemType Junction `
-Path "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-opencode-session-sync" `
-Target "$env:USERPROFILE\.dsh\plugins\dsh-opencode-session-sync"
```
Then edit `~/.dsh/profiles/desktop/package.json`:
<details>
<summary>Profile edits, and the macOS / Linux equivalents</summary>
```jsonc
{
"dependencies": {
"dsh-opencode-session-sync": "link:/absolute/path/to/dsh-opencode-session-sync"
},
"dsh": {
"profile": {
"bundles": [
// … existing bundles …
"dsh-opencode-session-sync"
]
}
}
}
```
macOS / Linux: replace the junction step with a symlink, and use the same `link:` path spelled for your platform.
```sh
git clone https://github.com/forwardzz/dsh-opencode-session-sync.git ~/.dsh/plugins/dsh-opencode-session-sync
ln -s ~/.dsh/plugins/dsh-opencode-session-sync ~/.dsh/profiles/desktop/node_modules/dsh-opencode-session-sync
```
Running `pnpm install` in the profile directory is equivalent: it materializes the link from the `link:` declaration. If your installation provides the `dsh` CLI, `dsh plugin --profile desktop add link:<absolute path>` is the same three steps in one command.
</details>
### Reload and Verify
Plugins are mounted at startup, so **restart DeepSeek Harness**. Adding a plugin or editing its config is not hot-reloaded.
From the clone, check the installation end to end (dependency, bundles entry, link target, patch file, entry module):
```sh
node tools/verify-install.mjs
```
It only reads. A passing run means DSH will find and load the plugin; it does not prove the plugin has already loaded.
## Usage
### Automatic sync (default)
About 4 seconds after startup the plugin imports once, then appends anything new on each later start. No action needed.
Two files record what happened:
| File | Contents |
| --- | --- |
| `~/.dsh/opencode-session-sync/last-run.json` | Full report of the most recent sync, per session, with reasons |
| `~/.dsh/opencode-session-sync/state.json` | Ledger: OpenCode session id → DSH session id, plus the last source row imported |
### Manual sync: the `opencode_sync` tool
The plugin registers one agent tool. Ask in conversation, or call it directly:
| `action` | What it does | Main parameters |
| --- | --- | --- |
| `list` | **Read-only** inventory: which OpenCode sessions exist, what workspace each maps to, and how many rows are still pending | `workspace`, `limit`, `includeChildren` |
| `import` | Syncs: new sessions are imported, existing ones get their new turns appended | `dryRun`, `workspace`, `sessionIds`, `limit`, `includeChildren` |
| `status` | Ledger and the last run | — |
Things you can say:
- "List the OpenCode sessions that are not in DSH yet."
- "Dry-run a sync and show me what it would do."
- "Sync only the sessions from `my-project`."
`list` reads both sides, so its status column tells you exactly what an `import` would do: `待导入` (will be created), `有新内容` with a pending count (will be appended), `已同步`, or `无正文`.
### Command line self-check
Convert and validate without starting DSH (read-only, writes nothing):
```sh
node tools/selfcheck.mjs # inventory + convert + structural checks
node tools/selfcheck.mjs --limit 5 # only the 5 most recently updated
node tools/selfcheck.mjs --all # include subagent child sessions
node tools/selfcheck.mjs --db <path> # point at a specific OpenCode database
```
Exit codes: `0` passed, `1` problems found (listed individually), `2` environment not satisfied (for example, no OpenCode database on this machine).
## Configuration
Configuration lives in `~/.dsh/opencode-session-sync.json`. Defaults are written on first run; changes take effect after a restart.
| Field | Default | Meaning |
| --- | --- | --- |
| `enabled` | `true` | When `false` the plugin does nothing at all: no tool, no sync |
| `dbPath` | empty (default location) | OpenCode database path |
| `autoSyncOnStart` | `true` | Sync once after startup |
| `autoSyncDelayMs` | `4000` | Delay before that first sync, to stay out of the startup path |
| `includeChildren` | `false` | Also import subagent child sessions |
| `importLimit` | `200` | At most this many sessions per run, newest first |
| `titlePrefix` | `"[OC] "` | Prefix added to imported titles; set `""` for none |
| `agentPreset` | `"standard"` | Agent preset recorded on imported sessions |
| `includeReasoning` | `true` | Keep reasoning blocks |
| `includeToolCalls` | `true` | Keep tool calls |
| `maxToolResultChars` | `200000` | Per-block character cap; longer blocks are truncated and marked |
| `createMissingWorkspaces` | `true` | Create a workspace when the target directory has none |
| `onlyExistingDirectories` | `false` | Skip sessions whose directory does not exist on this machine |
Default OpenCode database location:
- Windows: `%USERPROFILE%\.local\share\opencode\opencode.db`
- macOS / Linux: `~/.local/share/opencode/opencode.db`
## How sessions map to workspaces
| OpenCode | DSH | Notes |
| --- | --- | --- |
| `session_v2.directory` | `SessionHeader.cwd` | Decides the workspace, e.g. `C:/Users/me/proj` → workspace `C:\Users\me\proj` |
| (from `cwd`) | workspace registry | The session is registered in that workspace's list; the workspace is created if the directory has none |
| `session_v2.title` | `session/title` event | Prefixed by default so imported sessions stand out |
| `session_v2.time_created` | `SessionHeader.createdAt` | Original creation time is kept, so the list sorts by real time |
| `session_message` (`user`) | `user/message` | User text |
| `session_message` (`assistant`) | `assistant/message` + `tool/call` + `tool/result` | Reasoning, text and tool blocks are mapped separately; tool inputs and outputs are kept |
| `session_message` (`idle`) | `turn/end` | OpenCode's idle rows are what split turns |
| model info | `assistant/message.source` | `providerID` / `model.id` preserved |
| `tokens` | `usage` | Input, output, cache and reasoning tokens |
### What incremental sync appends
Already-imported sessions are recognised from the DSH log itself — the plugin writes the OpenCode row ids into the message ids, so it can tell which source rows are already present. A sync then:
1. checks that already-imported rows are still a prefix of the source (otherwise `diverged`, see [Troubleshooting](#troubleshooting));
2. cuts the source after the last imported row, keeping the `idle` rows that delimit turns;
3. builds events that continue the existing log — `seq` continues from the last event, `turn` numbers continue from the last turn;
4. appends them with `open(id, 'write')`, then refreshes the projection cache with the complete log.
Only the tail is ever appended. History already written to a DSH session is never rewritten.
## Troubleshooting
**`verify-install.mjs` reports a missing dependency or bundles entry.** The profile edit did not land, or you edited a different profile. Check that `dependencies` and `dsh.profile.bundles` both name `dsh-opencode-session-sync` in the profile you actually run.
**The plugin is installed, but DSH does not seem to load it.** Plugins mount at startup. Restart the app, then look for an `[opencode-session-sync]` line in its output. A bundle that fails to resolve is skipped with a warning instead of failing the boot, so a silent skip is possible — `verify-install.mjs` is what tells the two cases apart.
**Nothing was imported.** Check `last-run.json` first. Common reasons: the database path is wrong (`dbPath`), `enabled` is `false`, the session has no rows in `session_message` (see below), or the directory does not exist locally and `onlyExistingDirectories` is `true`.
**A session was skipped as "legacy-only".** That session has rows only in the older `message` / `part` tables and none in `session_message`. This plugin does not parse that layout; it reports the session instead of guessing.
**Sessions were imported, but the workspace list does not show them.** The workspace gets the session registered at sync time. If the registration failed (a `unattached` warning in the report), restarting DSH makes the workspace registry adopt every session whose `cwd` is under it.
**Rows appear without titles.** The list reads a persisted projection cache for cold sessions, which the plugin refreshes after each write. If that refresh failed it is recorded as a warning in the report, and the title appears once you open the session.
**A session reports `diverged`.** The source session was rewound or rewritten (or a ledger from an earlier import no longer matches), so appending would produce a transcript that never existed. The plugin skips it. To import the new shape, delete the corresponding DSH session and sync again — see [Uninstall](#uninstall) for the file layout.
**I upgraded the plugin and want to re-import an old session.** DSH's session persistence has no delete API, so the plugin cannot rewrite an existing session. Stop DSH, delete that session directory under `~/.dsh/sessions/--<workspace>--/` (the id is in `last-run.json` and `state.json`), then start DSH again.
## Execution Boundaries
The plugin only reads the OpenCode database and only writes through DSH's session persistence. It does not modify `opencode.db`, the OpenCode config, or any file outside `~/.dsh`.
**The import is one-way and text-only.** There is no reverse export back into OpenCode. Imported sessions are a faithful record, not a resumable execution state: tool calls are replayed to the model as history and are never re-executed.
**Subagent child sessions are not imported by default.** Reproducing OpenCode's parent/child nesting in DSH needs `subagent/catalog` facts written into the parent log, which this plugin does not do. `includeChildren: true` imports them as independent sessions and loses the nesting.
**Rewind detection needs the ledger.** The log alone proves which rows were imported, so a missing ledger still cannot cause duplicates. It can, however, miss a source that was rewound *and* extended on a different branch; that ledger entry (`lastRowId`) is what turns that case into `diverged`. Keep `state.json`.
<details>
<summary>Event mapping details, the projection cache, and what is not imported</summary>
**Not imported.** OpenCode rows of type `system` (tool-change notices), `synthetic` (harness reminders such as plan mode) and `compaction` (summaries) have no faithful DSH equivalent here, so they are skipped. `agent-switched` and `location-switched` are skipped as well. Every skipped type is counted in the report's `skippedEvents`, so nothing disappears silently.
**Turn and step structure.** OpenCode's `idle` rows delimit turns; within a turn, a user message and the assistant reply share one step, and a further user message opens the next step. That matches the shape DSH itself writes, which is what the checks in `lib/validate.js` assert.
**Why go through DSH's own writer.** `sessionPersistence.create()` / `open(id, 'write')` validate every event and the relationships between them, and the backend owns the framing (a log is a series of independent zstd frames). Writing bytes directly would break on the next session-format change; going through the writer means the plugin does not carry a format implementation at all.
**Why the projection cache is refreshed.** A session written straight to storage is a *cold* session. DSH's session list serves cold rows from a persisted projection cache, so a newly imported session would otherwise appear without its title. After each write the plugin calls `sessionProjectionCache.coldSnapshot(header, 0, log)` with the complete log, which folds and persists the record. This step is fail-soft: a failure is recorded as a warning and costs only the title hint until the session is opened.
**Truncation.** Blocks longer than `maxToolResultChars` are truncated with a marker appended (`[已截断 N 个字符,原始长度 M]`) and counted in the report, so an oversized tool output cannot quietly distort the record.
</details>
## Uninstall
1. Remove `dsh-opencode-session-sync` from `dependencies` and from `dsh.profile.bundles` in `~/.dsh/profiles/<profile>/package.json`.
2. Delete the link at `~/.dsh/profiles/<profile>/node_modules/dsh-opencode-session-sync` (deleting a link never touches the clone).
3. Restart DSH.
Imported sessions stay where they are. To remove them too, delete the relevant `~/.dsh/sessions/--<workspace>--/session-*/session.v4.jsonl.zstd` directories — the ids are listed in `~/.dsh/opencode-session-sync/state.json`. Uninstalling does not undo anything the plugin already imported.
## Development
```sh
npm test # unit tests (fixtures; no OpenCode database needed)
node tools/selfcheck.mjs # convert a real OpenCode database and validate the result
node tools/verify-install.mjs # check that the plugin is wired into a profile
```
Unit tests cover event sequence and numbering, turn/step splitting, tool calls and error results, duplicate call ids, reasoning and truncation switches, title fallback and title changes, directory normalisation, model fallback, and the incremental-append rules (seq and turn continuation, idle-delimited turns, no-op appends). Every case also runs the structural and cross-event checks in `lib/validate.js`.
Three stronger suites need the DSH application unpacked as a real dependency tree (`resources/app.asar` → an extractor is included in the workspace that hosts this plugin):
```sh
node test/integration.mjs <dshDir> <opencodeDb> # full sync flow, idempotency, ledger, buckets
node test/host-apply.mjs <dshDir> <opencodeDb> # loads the plugin in a real Cordis context and calls the tool
node test/incremental.mjs <dshDir> # import → grow → append → no-op → rewind, against a synthetic OpenCode database
```
`test/incremental.mjs` builds its own `opencode.db` with the real schema, so it can grow the source mid-test, which a real database cannot do.
CI runs on Ubuntu and Windows across Node 22 and 24: syntax checks over every file, the unit tests, and a behavioural check that `selfcheck.mjs` exits `2` with a clear message when no OpenCode database exists.
| Path | Responsibility |
| --- | --- |
| `lib/opencode-db.js` | Read-only SQLite access to `opencode.db` |
| `lib/convert.js` | Session rows → DSH events; first-import and append planning |
| `lib/validate.js` | Structural and cross-event checks shared by tests and self-check |
| `lib/sync.js` | Orchestration: read the existing log, decide, write, attach workspace, warm cache |
| `lib/ledger.js` | Config, ledger and run report on disk |
| `lib/index.js` | Plugin entry: registers `opencode_sync`, runs the startup sync |
| `tools/`, `test/` | Self-checks and the test suites above |
## License
[MIT](LICENSE) © forwardzz
Install
dsh plugin --profile web add github:forwardzz/dsh-opencode-session-sync
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-opencode-session-sync from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.