GitHub stars MIT License DeepSeek Harness plugin Platform: Windows (verified)

中文 · English

A resident agent entity for the DeepSeek Harness ecosystem

A bot that belongs to no session: its own instances, memory you can open and edit, a task queue, and an executor that stops at the boundary and waits for you.

## Documentation | Goal | Entry | | --- | --- | | Why this exists, and how it differs from "a session" | [Why a resident entity](docs/why-bot.md) | | Install, configure, day-to-day use | [User guide](docs/user-guide.md) | | All 7 tools: parameters, output, boundaries | [Tool reference](docs/tool-reference.md) | | How store / executor / memory tiers / approval gate fit together | [Architecture](docs/architecture.md) | | Against the 2026 assistant survey: what is built, what is not | [Feature matrix](docs/feature-matrix.md) | | All docs and how they divide the work | [Docs index](docs/README.md) | ## What it is `dsh-bot` gives [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) a kind of agent that **belongs to no session**: - **It outlives the conversation.** Close the session, compact the context, restart the machine — it is still there. Instances, memory, and the task queue live in `$DSH_HOME$DSH_HOME/bot/`, and every session reads and writes the same entity; - **Memory is a file you can open.** Not an invisible vector store, but a tree of `.md` files under `$DSH_HOME$DSH_HOME/bot/memory/`. Change one line and every later conversation follows it. Root files stay in context; subdirectories contribute only a name and one line of description; - **It moves things forward on its own.** A task queue plus an optional executor. Free time is **off by default**, and even when on it only acts when a **checkable condition** holds — its own queued work, an unanswered inbound message, a due reminder; - **It stops at the boundary.** Reaching outside the workspace parks the job in `awaiting` and **shows you the tool name and the full arguments**. You approve **one concrete action**, not a category. In one line: **install it and you get an entity that says "I remember that" — and can actually go do it.** ## Quick start The plugin installs into the profile you intend to use. ```sh # From a source checkout (one plugin, one repository) dsh plugin --profile web add # Or from npm, once published dsh plugin --profile web add dsh-bot ``` Then: a `bot` entry appears in the left sidebar. Open it and create an instance by type (companion / assistant / researcher / scribe are built in, and you can add your own), or step into one that already exists. The gear in the corner opens settings — memory, tasks, limits, connections, MCP servers, avatar, types. The agent then has 7 `bot_*` tools: | Goal | Tool | Notes | | --- | --- | --- | | See what state it is in | `bot_status` | Instances, heartbeat, queue, the model in effect and where that choice came from | | Have it remember something | `bot_remember` | Writes into its own memory files, with an origin label | | Ask what it remembers | `bot_recall` | Word search; Chinese is split into bigrams | | Have it go do something | `bot_task` | Queue it, schedule it, repeat it, continue or start fresh | | Have it say something outward | `bot_connector` | Through a user-configured connection (Telegram / HTTP) | | Use a credential once | `bot_secret` | The value never enters context; it goes straight to a virtual desktop | | Remind it of a moment | `bot_agenda` | Fires through a connector when due | Full list: [tool reference](#tool-reference). ## Main features

Memory is files, not a vector store

Memory lives as a tree of .md files under $DSH_HOME$DSH_HOME/bot/memory/ — visible, editable, and re-importable from disk in the settings page. Root files (MEMORY.md / SOUL.md / USER.md / MEMORY-CORE.md) stay in context; subdirectories contribute only a name and one line of description. The layering is the policy: moving a file between tiers is a file operation.

Every entry carries its origin

Entries record whether they came from user (they said it outright), agent (its own conclusion), or tool (read out of a file or a page). A claim that keeps its label is useful; the same claim wearing a fact's clothes is a hazard. Old data is read as agent — the more cautious reading.

Two gates for sensitive information

Hard gate: identity numbers, social security numbers, bank cards, passwords and keys, session tokens — refused at every setting, and it tells you the way around (use bot_secret rather than writing it down). Soft gate: health, finances, legal matters, family matters — excluded by default, one setting to allow, and anything kept carries the label forever after.

Memory has an age

Every entry can say how many days old it is. Past the threshold (90 by default) it is marked stale and labelled in the prompt. When the age cannot be computed it is -1, not a guess — unknown does not take part in a comparison, and that is deliberate.

Approval is a first-class state

Reaching outside the workspace parks the job in awaiting — a legitimate intermediate state, not an error branch. It stops with the tool name and the full arguments, so you approve a concrete action rather than "it wants to do something". Approving keeps it from stopping again: that was "yes, this time", not "you asked once".

One-step waiver, always expiring

Being asked the same thing repeatedly dulls a person, so a pass can be granted. A waiver matches the exact tool name only, and always carries an expiry — there is no "allow forever", because nobody can sign for their future self. Editing a job's title or note revokes approval: you approved the job you read at that moment.

Free time is reason-driven

Off by default. When on, idleness is only a precondition, never the reason itself. A checkable condition has to hold: its own queued work (the default), an unanswered inbound message, a due reminder, a file changed since it last looked. When nothing holds it says so plainly instead of quietly filling the time with activity.

Its own permission, three levels

read-only / full / chat, per instance. Free time defaults to read-only — acting on its own does not come with the ability to write unless you give it. The tool set follows the permission: a read-only instance never even sees the writing half.

MCP as a client, not hard-coded integrations

The plugin ships no MCP server, no address, no credential — it ships a client and an input box. Give it a streamable HTTP address and that server's tools appear in the ones it can call. Names follow the ecosystem's mcp__server__tool convention, so a name copied from any other client works unchanged. The test is a button, and its result stays on the status light.

A status light that does not lie

Next to each service: green only after a test that actually passed, red only after one that actually failed, grey before either. "Nobody looked" and "it is fine" are not the same claim, so the light does not merge them. Green keeps both timestamps underneath — "it worked at 14:02 and stopped at 14:31" is a different situation from "it never worked".

Two ways to repeat work

A recurring job can continue in its own context (carrying its last conclusion in — what a routine wants) or start a fresh one (from zero — what an audit wants). The reason: read your own previous conclusion first and a check quietly becomes a rubber stamp.

More than one way in

Inbound connections bring outside messages in: Telegram long polling, or a token-guarded HTTP endpoint. External messages are labelled with their origin and bounded in length (4000 characters by default, adjustable), because who said it and how long they went on are both part of judging it.

Every limit belongs to the user

No quota is hard-coded. How many messages are replayed, how many memory rows are searchable, how long one job may run, how much per day, how long an inbound message may be, concurrency and poll cadence — all in the settings page, defaults listed in one place. 0 means unlimited throughout.

Your own avatar and your own types

Four types are built in (companion / assistant / researcher / scribe) and their names, blurbs and personas are all editable — and you can add your own. The avatar accepts an uploaded image (PNG / JPEG / WebP / GIF up to 4 MB), validated by content type rather than by file name.

## Why this one - **It is not a longer session.** The usual move is to make the context bigger; this moves the state outside it. A session is temporary, an entity is persistent — confusing the two gets you the drawbacks of both. - **Governable beats clever.** Memory is files, origin is a label, limits are settings, approval is a state. Each one gives "why is it like this" an answer you can look up instead of a conclusion you have to trust. - **The dangerous parts are made visible.** Every screen shows what it is asking for, when it last worked, and who said this. **The dangerous failure is not doing the wrong thing — it is doing the wrong thing quietly.** ## Tool reference | Tool | Purpose | Reaches out | | --- | --- | --- | | `bot_status` | Instance list, heartbeat, queue, the model in effect and where it came from | – | | `bot_remember` | Record a durable entry (`text`, optional `kind` / `source`) | – | | `bot_recall` | Search memory by word (Chinese split into bigrams; filterable by kind) | – | | `bot_task` | Queue: add / schedule / repeat / continue-or-fresh / cancel | – | | `bot_connector` | Have the instance speak outward through a user connection | ✅ | | `bot_secret` | Deliver a credential into a virtual desktop (value never enters context) | ✅ | | `bot_agenda` | Note what should happen at a moment; fires through a connector | ✅ | > "Reaches out" means data leaves the machine — the other four only read and write `$DSH_HOME$DSH_HOME/bot/`. ### Where a memory lands `bot_remember` routes by kind: `focus` to `USER.md` (what the user cares about), `decision` and `fact` to `MEMORY-CORE.md`, everything else to `notes/YYYY-MM.md`. **That is policy, not implementation detail**: the files that stay in context hold only what can change behaviour. ### How it reads its own memory Root files enter the system prompt in full; every file in a subdirectory contributes only the `name` and `description` of its frontmatter. So **a good description on a note is worth more than a long body** — it decides whether the note is ever thought of. ## Configuration The plugin mounts through `cordis.patch.yml` as **a single row** (registered under the bare package name). Almost everything a user changes lives in the settings page rather than in that file: | Location | Contents | | --- | --- | | `$DSH_HOME$DSH_HOME/bot/bot.json` | Instances, types, memory index, tasks, connections, MCP servers, limits, rules | | `$DSH_HOME$DSH_HOME/bot/memory/` | The memory itself (a tree of `.md` files, editable directly) | | `$DSH_HOME$DSH_HOME/bot/avatars/` | Uploaded avatar images | Settings sections: instances and types · memory · tasks and queue · free time · limits · rules · connections · MCP servers · avatar · agenda. **Limits** (all defaulted, all editable, `0` means unlimited throughout): | Setting | Default | Meaning | | --- | --- | --- | | Job time limit | 30 min | How long one background job may run | | Daily total | unlimited | Background minutes per day | | Tool rounds per job | 8 | How many tool calls one job may make | | Queue entries | 300 | Rows kept; oldest fall off | | Messages replayed | 12 | Transcript messages in the system prompt | | Panel window | 60 | Messages the panel loads at once | | Searchable memory rows | 200 | Index bound; **the files themselves are never trimmed** | | Inbound length | 4000 chars | Truncated and labelled beyond | | Stale threshold | 90 days | Past this, marked stale | | Sensitive mode | exclude | `exclude` / `keep` (the hard gate is unaffected) | **Only two settings have bounds** — free-time concurrency and poll cadence — because they are mechanism parameters rather than quotas. ## How it works ``` Session side (model) Host side (plugin) Disk bot_* tools ──→ handlers registered on ctx.tools ──→ $DSH_HOME$DSH_HOME/bot/bot.json │ └─ memory/*.md ├─→ executor (optional, off) └─ avatars/ │ └─ queue → one model session → tool calls ├─→ approval gate (workspace verdict) └─→ inbound connections (Telegram poll / HTTP endpoint) Web side (browser) The same HTTP routes sidebar entry ──→ /api/bot.state (read-only poll, 3s) bot page ──→ /api/bot.manage (writes) settings page ──→ the same routes ``` - **Both halves ship together**: `impl.js` is the host (store, executor, tools, routes), `client.js` is the web half (sidebar, page, settings). The client half registers through `window.__ModuleLoader__.load()` and imports no `@deepseek-ai/dsh-client-*` package; - **One poll feeds every surface**: every sidebar entry and the page subscribe to the same `/api/bot.state`, so they cannot contradict each other; - **The workspace boundary is decidable**: `workspaceVerdict(toolName, args, workspace)` is a pure function returning `inside` / `guarded` / `refused` / `unchecked`. **`unchecked` is the important one** — "we checked and it was fine" and "there was nothing here to check" have to stay distinguishable in the code, or the former will impersonate the latter. ## Requirements - DeepSeek Harness (dsh) with a `web` profile installed - Node.js ≥ 22.19 - A working model (the plugin follows DSH's default model; nothing separate to configure) ### Verified versions | Component | Version | | --- | --- | | DeepSeek Harness (dsh) | `0.2.0-rc.2` (peer range `>=0.2.0-rc.1 <0.3.0`) | | Node.js | `22.20.0` | | Operating system | Windows (10.0.26200) | | dsh-bot | `1.0.0` | > **Verified on Windows only** (macOS / Linux untested, not claimed). The host half has no platform-specific branches, but untested is untested. ## Known limitations - **No remote execution.** The executor runs inside the host process; the machine goes down, it stops. That is structural for a local plugin, not a to-do — "keeps running while the machine sleeps" needs a cloud, and this version does not have one. - **No vector retrieval.** Memory enters context by file tier; there is no automatic switch to search past a threshold. At the current scale, adding retrieval would only add a less explainable layer — the cost is a size ceiling. - **No browser integration.** The browser plugin that ships with DSH is session-scoped, not part of this resident entity. - **No voice, no cross-device.** Single machine. - **No dedicated email entry point.** Telegram and HTTP inbound exist; "forward an email to dispatch work" does not. - **The sensitive-memory prompt is only half done.** Sensitive entries are **refused** rather than **asked about**; "no retroactivity" holds in the implementation (changing the setting never alters existing entries) but the UI does not say so. - **MCP is client-only and minimal**: `initialize` / `tools/list` / `tools/call`. No `resources`, no `prompts`, no subscriptions, no OAuth flow (put a token in the request-header field). Long-running work does not use the official `tasks` extension. - **Streamable HTTP only** for MCP transport; a stdio server needs a wrapper. - **Inbound defence is bounding and labelling, not content filtering.** The public record is unkind to detection-based defences (most were bypassed >90% under adaptive attack). So this picks the two that keep working: **bound the size, label the source**. - **Waivers match the exact tool name.** Changing one argument misses. That is the conservative side; the cost is being asked again for the same kind of operation with different arguments. - **`bot_secret` needs the virtual-desktop plugin** (`dsh-vdesktop`). Without it the tool says so plainly rather than failing silently. ## Development Plain JavaScript, no build step — `impl.js` / `client.js` / `entry.js` are what runs. ```sh # Offline verification suite (does not need dsh running) node tests/verify-bot-plugin.mjs # Client wiring — run after touching the UI node scripts/check-client-wiring.mjs # Audit against the research report node scripts/audit-against-research.mjs ``` Code layout: | File | Responsibility | | --- | --- | | `entry.js` | Thin shell. `stat()`s the implementation and stamps its mtime into the import URL, so editing `impl.js` does not need a host restart. **Its exported `inject` is the one the host reads** (see below) | | `impl.js` | All host logic: store, memory file layer, workspace verdict, executor, approval, connections, MCP client, routes, the 7 tools | | `client.js` | All web: sidebar entry, bot page, settings page. Carries its own styles and depends on no client package's internal class names | | `scripts/` | Two checks, a runnable MCP demo server, desktop probes | | `tests/` | End-to-end verification (HTTP routes + store semantics + tool behaviour) | > **A trap worth repeating**: Cordis reads the `inject` exported by the **entry module**, not the one in the implementation file. When they disagree the plugin starts without a service it needs and fails on first use with `cannot get property ... without inject`. The two must match word for word, and the verification suite asserts it. ## Changelog See [CHANGELOG.md](CHANGELOG.md). This plugin has no versioned release yet; that file records development rounds. ## Credits Thanks to the [DeepSeek Harness repository](https://github.com/deepseek-ai/deepseek-harness) and the DeepSeek AI team: this plugin's plugin system, tool runtime, and model interface are all built on that project. Thanks also to [Cordis](https://github.com/cordiverse/cordis) for the plugin foundation. The **research behind this plugin** drew on public documentation from the 2026 generation of personal assistants and agents; the item-by-item comparison is in the [feature matrix](docs/feature-matrix.md). ## License [MIT License](LICENSE). > A community plugin for DeepSeek Harness, not an official DeepSeek product.