Skip to content
dsh.fish
Bundle

dsh-memoryhub

MemoryHub (mh) integration for DeepSeek Harness: loads .memoryhub checkpoint memory on session start, saves dsh sessions back into checkpoints, and registers the mh workflow skill and tools

Source
solknight48
stars
2 stars
License
MIT
Updated
Updated 10 days ago

Readme

# 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/<name>/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=<id>`.
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:<port>/?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 <uiBudget>` (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 (`<!-- mh | loaded:` and friends); a failed load
   arms nothing.
2. Writes it as a pi-format transcript at
   `$TMPDIR/dsh-memoryhub/dsh_<session-id>.jsonl`.
3. Runs `mh save --transcript <that file>` (or
   `mh save --compact --file <summary.md> --transcript <that file>` with the
   summary the model wrote into the tool call).

Consequences worth knowing:

- The checkpoint filename key follows mh's pi rule (`pi-<id12>`) even though
  the session came from dsh. Cosmetic only; identity is stable per session.
- The bridge is rebuilt at every save, so a later save sees the whole session
  up to that point and replaces the earlier representation (mh keeps one
  representation per session).
- Steering messages and file attachments are not bridged (v1); a compact
  save's summary can still carry anything important.
- `mh import` backfills Claude Code / pi / Codex history only — dsh history
  enters checkpoints by being saved through this plugin.

## Verify

```sh
npm install
npm run build
npm test        # e2e against the real mh CLI in a temp HOME
```

The test suite exercises the exact paths the plugin uses: bridge JSONL →
`mh save --transcript` → `mh load`, compact-replaces-purified,
save-twice-replaces, and the mh-ui pool against a real `mh ui` server
(spawn, token URL, page 200, guard 403, no-hub mapping). Plugin
registration (six tools + `mh(runtime)` skill + the Memory tab's client
bundle in the boot graph and the `/plugins/memoryhub/mh-ui` route branches)
was verified by booting a real dsh web profile with the bundle installed.
The `agent/session-start` → `mh load` → `agent.inject` path uses the same
extension points as dsh's own hook bridges; it has **not** been exercised
against a live model session yet (needs an API key), and neither has the
Memory tab's in-browser render.

## Layout

```
src/index.ts   plugin: config schema, session-start auto-load, tools, skill,
               mh-ui route (webServer soft dependency)
src/bridge.ts  dsh session events -> pi-format JSONL transcript
src/mh.ts      execFile runner for the mh CLI (non-zero exits are data)
src/mh-ui.ts   mh ui process pool (one map server per workspace)
src/estimate.ts context-share estimate for mh_load (adapter window, mh's heuristic)
src/skill.ts   the mh workflow skill, adapted for dsh
src/client/    browser half: the Memory conversation-view tab (iframe of mh ui)
tests/         e2e against the real mh binary
```

## License

MIT

Install

dsh plugin --profile web add github:solknight48/dsh-memoryhub

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source