# dsh-memoryhub English | [简体中文](README.zh-CN.md) [MemoryHub](https://github.com/solknight48/memoryhub) (`mh`) integration for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`). MemoryHub keeps project memory as **purified sessions in git-versioned checkpoints** under `.memoryhub/`. This plugin wires that into dsh: - **Auto-load on session start** — runs `mh load` in the session workspace and injects the checkpoint memory as durable plugin context. No prompt, no tool call; the model simply starts with its memory back. - **`mh_save` bridges dsh sessions to mh** — dsh session files are not among the transcript formats mh discovers (Claude Code / pi / Codex), so the plugin renders the live session's durable event log as a pi-format JSONL transcript (in the temp dir) and saves through mh's existing `--transcript` path. Purified saves and agent-written compact saves both work, under a stable per-session identity: re-saving **replaces**, never duplicates. - **Six tools** — `mh_load`, `mh_save`, `mh_status`, `mh_list`, `mh_search`, `mh_checkpoint` wrap the CLI; the model never has to shell out for the common operations. Rarer operations (`mh link`, `mh back`, `mh import`, `mh ui`) stay in the shell, as the skill explains. - **A "Memory" tab in the web UI** — beside chat and trajectory: the `mh ui` checkpoint map of the session's workspace, embedded live (see [The Memory tab](#the-memory-tab-web-ui)). - **The `mh` workflow skill** — registered at runtime, it teaches the model when to load, when and how to save (including writing compact summaries), and the hub rules (no HANDOFF.md files, `git -C .memoryhub` for anything `mh` does not cover). ![Memory tab in the dsh web UI](docs/memory-tab.png) ## Prerequisites `mh` installed and on PATH (`uv tool install git+https://github.com/solknight48/memoryhub`; needs git ≥ 2.32, Python ≥ 3.12). The plugin shells out to it — the hub format, git commits, and error wording stay owned by mh. ## Install ```sh dsh plugin --profile web add github:solknight48/dsh-memoryhub ``` The package declares `dsh.bundle`, so the install appends its patch layer to the profile. For a checkout instead: `dsh plugin --profile web add ./dsh-memoryhub`. Git installs fetch sources, and pnpm ≥ 10 asks before running this package's `prepare` build the first time; allowlist `dsh-memoryhub` in the profile's `pnpm-workspace.yaml` as `dsh` instructs, then re-run the add. ## Configure Every field is optional; these are the defaults: ```yaml # $DSH_HOME/profiles//cordis.patch.yml — restates the whole row. - insert: - id: memoryhub name: dsh-memoryhub config: mhBin: mh # mh executable (PATH-resolved) autoLoad: true # mh load + inject on every session start # loadBudget: 6000 # token budget for auto-load; omit for mh's default timeoutMs: 20000 # kill any mh invocation after this registerTools: true # the six mh_* tools registerSkill: true # the mh workflow skill noHubHint: false # inject a one-line hint when no hub is found uiTab: true # serve the mh-ui URL route the web Memory tab fetches uiReadOnly: false # spawn mh ui --read-only for the Memory tab uiBudget: none # the Memory tab map's initial budget ('none' = no over-budget badges) contextWindowTokens: 128000 # fallback window for the context-% estimate ``` With `noHubHint: false`, a workspace without `.memoryhub/` stays completely silent: no injection, no log spam. mh itself decides what "the project" is by walking up from the session's recorded cwd — the same rule as in a shell. ## Loading: no budget by default, with a context-% receipt `mh_load` loads **every session** in the selected checkpoints — the over-budget filtering of stock `mh load` is off by deployment choice. Pass the tool's `budget` argument when you do want only the newest sessions that fit. (Auto-load, if you re-enable it, keeps its budgeted semantics via `loadBudget`: it injects into every session silently, so it stays opt-in per token.) Every successful `mh_load` ends with a receipt line, e.g.: ``` [memoryhub] memory ≈ 31,240 tokens ≈ 12.2% of the 256,000-token context window (adapter-reported); session total after load ≈ 18.6% ``` The window comes from the model adapter's own report (the session's newest `request/context` event) when available, otherwise from `contextWindowTokens`. The "session total" adds the last request's measured input size, so it reads as "where this load leaves the conversation". Token counting uses mh's own ~4 chars/token heuristic, so the number agrees with the Memory tab's sizes. ## The Memory tab (web UI) The package is a dual-face dsh plugin: the same `memoryhub` row that loads the host half also puts the browser half (`dsh.client` in `package.json`) into the web boot graph. The browser half registers one entry into the `conversation.view` slot ring — a **Memory** tab next to chat and trajectory. What the tab shows is not a reimplementation: it is `mh ui` itself, the checkpoint map (timeline, token budget, per-turn editing), embedded in an iframe so every mh feature and fix shows up unchanged. The wiring: 1. The tab asks the host half for the map URL of its session: `GET /plugins/memoryhub/mh-ui?session=`. 2. The host half resolves the session's workspace (`session.header.cwd`) and lazily spawns `mh ui --no-browser --port 0` there — one server per workspace, pooled, killed when the plugin unloads. mh keeps its own hub discovery (walk up for `.memoryhub/`, `MH_HUB` override). 3. The token-bearing URL (`http://127.0.0.1:/?t=…`) is parsed from the child's stdout and returned to the tab, which iframes it. mh's own security model (loopback bind, one-shot token, Host check) applies unchanged; `uiReadOnly: true` serves the map without editing. A workspace without a hub renders an empty state with a retry button instead of a server. The route is registered only when a web server exists, so headless compositions never notice the feature. The map is spawned with `mh ui --budget ` (default `none`, which needs mh from the memoryhub repo with `mh ui --budget` support — 2026-08-14 or later): the map's budget box starts empty, so its over-budget preview badges stay off, matching `mh_load`'s load-everything default. Set `uiBudget` to a number to restore the budgeted preview. ## How saving works `mh save` identifies a session through its transcript. mh knows Claude Code, pi, and Codex transcript formats; dsh is not one of them (yet). So on `mh_save`, the plugin: 1. Walks the session's durable event log and keeps exactly what mh's own purifier keeps: genuine user input (`source.kind === 'user'`) and assistant text. Plugin-injected context (including this plugin's own auto-load snapshot), tool calls/results, and reasoning stay out. Assistant text that **follows an mh load** is dropped too, until the next genuine user message: that reply is the load receipt plus a digest of the memory that was just loaded — content that already lives in the checkpoints. Saving it would re-embed old memory into every new session and snowball. The load is detected whether it went through the `mh_load` tool directly or through a `run_code` block (the web GUI's path); the user's own request line stays. Detection is **two-key**: the call must have a load's shape (a direct `mh_load` call, `tools.mh_load(…)` in code, or `mh load` as a shell command — text that merely mentions mh load in a grep or edit payload does not count) AND the call's result must carry mh's own load receipt (`