Native memory for DeepSeek Harness — auditable facts with evidence chains, powered by StateCore.
dsh-statecore is a dsh plugin, not an MCP server: it mounts StateCore's memory engine straight onto dsh's own plugin context, so memory participates in dsh's session log, permission system, and Code Mode the same way any other native capability does.
What it does
Auto-ingest — every user/message and assistant/message in a session is fed into per-project memory as it happens, with no model cooperation required. An MCP server can only be pull: a model has to decide to call a write tool. This plugin also pushes.
Auto-inject — each session's agent/pre-step waterfall folds a budgeted digest of project memory into the model's context automatically, the same fold pattern @deepseek-ai/dsh-agent-instructions uses for AGENTS.md/CLAUDE.md instructions.
Five native tools, plus why they're native, not MCP — remember/recall/facts/why/forget register through dsh-tools, so they carry honest JSON output.schema (Code Mode can await tools.why({factId}) for a structured evidence chain, not just text), participate in dsh's permission and presentation pipeline, and unregister cleanly on plugin dispose (HMR-safe).
Digest — the background pass that consolidates raw conversational events into stable, auditable facts — runs on the host's own configured model, through ctx.llm. There is no separate API key to configure: whatever model you've already pointed dsh at drives memory distillation too.
Install
Three ways to enable the plugin, in order of how committed you are to using it.
1. --patch overlay (fastest way to try it)
npm install --global dsh-statecore
dsh web --patch "$(npm root -g)/dsh-statecore/statecore.cordis.yml"
Or clone this repository and point --patch at its directly. This is the same pattern 's own overlays use. Nothing persists across runs; add again next time, or move to route 2.
dsh plugin --profile web add dsh-statecore
dsh --profile web
dsh plugin add installs the package into the profile's own node_modules via pnpm, then reconciles dsh.profile.bundles: because this package's package.json declares "dsh": { "bundle": { "patch": "./statecore.cordis.yml" } }, it joins the profile's bundle layer stack automatically — no manual --patch needed on later runs.
For a profile built without the interactive dsh plugin add flow (CI, infra-as-code), add the package as an ordinary dependency in the profile's package.json and append it to dsh.profile.bundles directly:
Then pnpm install in the profile directory before dsh --profile . This is the same shape dsh plugin add produces for you in route 2 — use this route when a build step manages the profile directory instead of a human running pnpm.
Configuration
All fields live under the mounted plugin's config: in statecore.cordis.yml (see the shipped file for the commented defaults).
Field
Default
Description
dataDir
~/.statecore
Embedded SQLite store directory. Ignored when url is set. Shared with statecore-mcp's own default.
url
unset
Talk to a self-hosted StateCore deployment over HTTP instead of the embedded backend. Unlocks Postgres/pgvector semantic retrieval.
injectBudget
4000
Characters of recalled memory (digest + facts) injected per turn. The rendered wrapper adds roughly 100 more characters on top, unbounded by this field.
inject
true
Fold recalled project memory into the model context every step.
ingest
true
Auto-ingest session messages into memory.
digest
true
Run digest passes over ctx.llm. false means this plugin never calls ctx.llm at all.
digestThreshold
20
Pending ingested events before a digest pass runs.
digestProvider
unset
Pin the digest pipeline's llm provider. Must be set together with digestModel. Unset resolves to whichever provider registered first.
digestModel
unset
Pin the digest pipeline's llm model id. Must be set together with digestProvider.
Model Experience
Injected project memory
What the model sees
When config.inject is true and a project has recalled memory, each eligible agent/pre-step folds one UserMessage into the entering batch, right after the step's own claimed prompt (mirroring dsh-agent-instructions's fold position). The message wraps recall({maxChars: injectBudget})'s digest and fact list.
Verbatim text for this field, when needed
## Project memory (StateCore)
- []
- []
Use the `why`, `facts`, and `recall` tools to verify or explore this memory further.
Token effect
Capped by injectBudget characters of recalled body plus roughly 100 characters of wrapper text (the header and the closing tool-pointer line, unbounded by injectBudget). Not added on every step: a content fingerprint (the digest text plus the set of fact ids) is cached per session, so an unchanged memory version is never re-spliced — only a step where the recalled memory actually changed pays this cost again.
KV Cache effect
Append-only. Each distinct memory version adds one new message after the existing reusable prefix and does not invalidate earlier KV-cache entries. Because nothing removes an earlier injected message, distinct memory versions accumulate across a session's lifetime until compaction shadows the earlier ones — the same accumulation shape @deepseek-ai/dsh-time-context documents for its own positive-interval readings.
Five tool definitions (name, description, JSON parameter schema, JSON output.schema) enter the tool-definition preamble of every request once the plugin is mounted — see src/tools.ts for the exact schemas. Descriptions are reused verbatim from statecore-mcp's own MCP tool registrations, so a model sees identical guidance whether the backend is reached through MCP or natively.
Token effect
Fixed direct token effect for every request while the plugin is mounted: all five schemas register together, with no per-tool config toggle (mounting the plugin is the opt-in for all five).
KV Cache effect
Independent of turn content. Tool schemas are part of the request's tool-definition preamble, not the conversation transcript; they stay stable across steps within a turn (the registered tool set does not change mid-session), so they do not themselves invalidate a reusable prefix.
Digest pipeline llm calls
What the model sees
Nothing, directly. When config.digest is true and a scope's pending ingested-event count crosses digestThreshold, src/llm-bridge.ts's createDigestLlm issues a separate ctx.llm.stream() call — using digestProvider/digestModel when pinned, else the first registered provider — to classify and consolidate pending events into facts. This call is never appended to the user's own conversation.
Token effect
Zero direct token effect on the user's own request. A separate request against the host's already-configured ctx.llm capacity and billing, sized by the pending event batch (bounded by digestThreshold), not by the user's context window.
KV Cache effect
Independent model request: a distinct ctx.llm.stream() call outside the user's own conversation history, so it neither reuses nor invalidates the user's request KV cache.
Shared memory with statecore-mcp
dsh-statecore and statecore-mcp both read and write the same embedded ~/.statecore SQLite store by default (same scope-resolution rule: git root, else the working directory). A fact dsh remembers is visible to Claude Code over MCP, and vice versa, with no sync step:
# In dsh, in this project:
# "Remember that our validation drink is lapsang-42."
# In Claude Code, connected to statecore-mcp, in the same project:
claude mcp add statecore -- npx -y statecore-mcp
# "What's our validation drink? Check memory." -> lapsang-42
^22.19.0 || >=24.0.0 (inherited from the dsh host's own engine floor; statecore-mcp itself supports Node >=20 standalone)
Yes
deepseek-harness is pre-release: every @deepseek-ai/* package pins an exact rc, and a dsh rc bump that changes plugin-facing APIs needs a matching dsh-statecore patch release. There is no compatibility promise across rc boundaries yet.
Building from source right now
package.json's statecore-mcp dependency is currently file:../StateCore/apps/mcp — a git clone of this repo only works standalone once statecore-mcp@0.2.0 (the version carrying the statecore-mcp/lib entry this package imports) is published to npm and the dependency is swapped to ^0.2.0. Until then, building from source requires ~/Code/StateCore-App/StateCore (or wherever you clone yul761/StateCore) checked out as a sibling directory, built (pnpm install && pnpm run db:generate:lite && pnpm run build), before npm install here resolves. See RELEASING.md for the exact human-gate publish sequence that retires this step.
Known Limitations and Deferred Work
Tool scope follows the calling agent's session; a headless, single-scope process is unchanged.src/tools.ts's registerTools(ctx, config, pool) resolves a fresh MemoryBackend per call, from exec.agent.session through the same createScopeCacheregisterIngest/registerInject already use, falling back to the process default scope (resolveDshScope) only when a call carries no agent (a direct ctx.tools.execute() call outside an agent loop). In a single-workspace dsh process, or any call with a live agent, this always resolves to the calling session's own project. Only a tool call genuinely made with no agent at all falls back to the process default.
This is a pre-release integration against a pre-release host.deepseek-harness has no compatibility promise across rc boundaries (see the matrix above), and this package has not yet exercised a host rc bump itself.
Embedded/lite retrieval is keyword + CJK bigram, not semantic. The default embedded backend runs on SQLite with no pgvector; recall still returns a budgeted digest, believed facts, and matching events, but won't find a paraphrase with no matching tokens. Semantic retrieval needs url pointed at a full StateCore deployment.
Distillation needs ctx.llm mounted, even when digest is left at its default true and no other plugin in the profile happens to need an llm provider — inject = ['tools', 'llm', 'agents', 'sessions'] is a fixed, not config-conditional, requirement list, so a profile with no llm adapter mounted fails to load this plugin at all, not just its digest path.
session/event ingest covers only user/message/assistant/message. Tool calls and results are never ingested directly. A fact recalled back into a later user/message (through injection) does get re-ingested as ordinary conversation text — an accepted first-order approximation, not a loop this plugin tries to break.
No per-tool disable.Config gates inject/ingest/digest as three whole-plugin switches; there is no flag to keep, say, remember/recall mounted while dropping forget.
More
StateCore — the memory engine this plugin and statecore-mcp both front
statecore-mcp — the MCP front end for the same engine, for every other MCP-speaking host