@ian_p/dsh-gentle-engram
v0.6.1
Published
HTTP-native Engram persistent memory integration for DeepSeek Harness
Maintainers
Readme
dsh-gentle-engram
HTTP-native Engram persistent memory for
DeepSeek Harness. The plugin registers native mem_* tools, captures prompts and tool
learnings, and keeps a session's memory alive across compactions.
This is a 0.2.0 rewrite. Version 0.1.x bridged Engram's MCP server; that approach could not be made correct, for reasons recorded in DESIGN.md.
How it works
The plugin talks to engram serve over HTTP and inserts no MCP row: it owns the mem_*
tool surface itself, and attaches the calling session's identity to every operation.
| Capability | How |
| --- | --- |
| Resolve the project from this session's directory | GET /project/current?cwd= with the session's own working directory, so the answer belongs to that session rather than to a shared child process; it is resolved once per session and gates writes and scopes recall |
| Recall project memory strictly on demand | the static protocol is injected into every request, and the model pulls memory with mem_context / mem_search — nothing is fetched or injected at session start |
| A tool surface that travels with the package | the mem_* tools are registered through ctx.tools.register, so their schemas, output shape and error handling are defined and tested in this repository |
The memory flow
session start resolve the project, gate writes and recall (nothing is written or injected)
recall pull project memory on demand with mem_context / mem_search
first write create the Engram session row, then attribute the write to it
every turn capture the user prompt, and any tool result carrying a Key Learnings section
compaction archive the summary, then inject outcome-specific recovery guidance
disposal release local state; the Engram row stays open and reusableMemory is pull-based. The static protocol is injected on every request, and project
memory is retrieved on demand with mem_context / mem_search; nothing is fetched or
injected at session start, so a new session begins with no memory in context until the model
asks for it. After a compaction, the one-shot recovery notice is the only automatic
memory-related text.
Requirements
- DeepSeek Harness 0.1.5-rc.2 or later.
- An Engram binary on
PATH(orENGRAM_BIN) that provides theserveandinstance-idsubcommands. Verify withengram serve,engram instance-idandengram --version.
When no ENGRAM_URL is configured the plugin owns the server implicitly and verifies before
attaching that /health reports the same instance_id as engram instance-id on this
machine. A server that is not this machine's own Engram instance - a foreign id, a missing
field, an unreadable local id - is refused, not adopted. Setting ENGRAM_URL chooses the
server explicitly and skips both the check and the instance-id read.
Installation
The published package:
dsh plugin --profile web add @ian_p/dsh-gentle-engramOr a local checkout, which links node_modules to your working tree so a rebuild takes
effect without reinstalling:
dsh plugin --profile web add /path/to/dsh-gentle-engramRestart DeepSeek Harness afterwards. The package is a Host bundle: its
cordis.patch.yml owns the lifecycle plugin and the tool surface.
Do not add a second Engram MCP row. Two rows with the same
serverNamein one scope fail the profile at load. This bundle intentionally contributes none.
Configuration
Set on the inserted row in your profile (or the host patch layer):
- insert:
- id: engram-memory
name: '@ian_p/dsh-gentle-engram'
config:
binary: engram # Engram executable used to spawn serve
captureToolResults: true
capturePrompts: trueEnvironment variables (the first three share the upstream Pi adapter's names; ENGRAM_HTTP_TOKEN
is Engram's own):
| Variable | Effect |
| --- | --- |
| ENGRAM_URL | Use an already-running server. The plugin then never spawns or restarts one, and skips the ownership check. |
| ENGRAM_BIN | Engram executable path. |
| ENGRAM_PORT | Port for the implicitly owned server (default 7437). |
| ENGRAM_HTTP_TOKEN | Bearer token for a token-protected Engram server. Every request the plugin sends to the Engram API carries Authorization: Bearer <token>; the /health ownership probe is the one exception, and Engram leaves that route open. Read per request, so changing it needs no restart. Leave it unset for the zero-config default (no header, server open). |
Tools
Twenty-one native tools, matching Engram's agent MCP profile except where noted:
mem_save, mem_search, mem_context, mem_stats, mem_timeline,
mem_session_summary, mem_session_start, mem_session_end, mem_get_observation,
mem_suggest_topic_key, mem_capture_passive, mem_save_prompt, mem_update, mem_delete,
mem_current_project, mem_judge, mem_compare, mem_doctor, mem_review, mem_pin,
mem_unpin.
mem_search, mem_context, mem_stats, mem_timeline and mem_review list read from
this session's resolved project. Cross-project reads (all_projects: true, also on
mem_stats and mem_review list) are explicit-only: the plugin never falls back to them
automatically.
Two deliberate differences from the MCP originals:
mem_list_projectsis absent. Its handler calls the store directly and Engram exposes no HTTP route for it. Usemem_searchwithall_projects: trueinstead.mem_session_start/mem_session_endtake no model-supplied session id. Session identity belongs to the plugin; letting the model mint keys would desynchronise every later capture.
One transport difference from the upstream Pi adapter: this plugin sends
Authorization: Bearer $ENGRAM_HTTP_TOKEN when that variable is set. Engram guards
DELETE /observations/{id}, DELETE /prompts/{id}, DELETE /sessions/{id}, GET /export and
POST /import with requireAuth and leaves every other route open, so without the header
mem_delete would be the one exposed tool that fails on a token-protected server. With the
variable unset the header is omitted and Engram's zero-config default applies.
Multi-repository workspaces
Engram refuses to guess which project a directory belongs to. If you start DeepSeek Harness from a directory containing several repositories, project resolution fails and no memory is written - deliberately, rather than filing memories under the wrong project.
Fix it by adding a config file at the workspace root:
{ "project_name": "my-project" }The plugin logs an actionable warning when this happens; it never injects the error text into the model's context.
Private blocks
Wrap anything that must not be persisted:
<private>
do not store this verbatim
</private>Redaction replaces the block with [REDACTED] and applies recursively to every outgoing
string. This is a convenience convention, not a secret scanner - it will not detect
credentials you did not mark.
Compaction recovery
When the harness compacts a session, the summary is archived to Engram and the next turn receives outcome-specific guidance with four possible states:
- Confirmed - already saved; no manual action needed.
- Failed - a manual
mem_session_summaryfallback is offered. - Unknown (timeout) - the write may have landed; verify with
mem_searchormem_doctorbefore retrying, never blindly. - Unavailable - no trustworthy session or project, so nothing was archived.
Session identity
The Engram session key is the harness agent id, and it survives resume and a plugin reload, so
one DSH session keeps one Engram row - unless that key had to be replaced (see below), in which case
a later reload starts from the agent id again and may mint another row.
Liveness is Engram's own runtime lease: POST /sessions is create-or-renew, and write activity
refreshes the 30-minute lease the server keeps on the row, at most once a minute. A lapsed lease
changes nothing about the session - it only stops the row from being offered as a live candidate
when some other writer omits a session id - and the next write renews it. Disposal therefore
releases local state and writes nothing: ending the row here would be terminal, and Engram can
never reopen it.
Ending is reserved for mem_session_end, where the model or the user declares the work over. It
is the only thing that sets ended_at, and the only exit from the key: the next write detects the
ended row, mints a fresh key, and files the memory under a new Engram session rather than
reopening the closed one. Work that continues after an explicit end is not lost, and the closed
row keeps its own end time and summary.
A row that disappears or is closed behind the plugin's back (engram delete session, the CLI) is
handled by the same re-registration, re-run at most once a minute: the row is re-created, or
replaced if it had ended. Engram answers a re-registration on an ended row with
409 session_already_ended, and a key whose persisted project differs with
409 session_project_conflict; both mean the key cannot carry this session any further, so the
plugin rotates once and retries. Any other failure is treated as transient and leaves a session
that was already working alone. The plugin does not trust a registration it confirmed a minute ago.
Registration is lazy: the row is created by the first thing that actually produces
memory — a mem_* write, a captured prompt, a captured tool result, or a compaction
archive — and never by session start. Resolving the project is read-only, so an agent the
harness merely publishes (a workspace the GUI
reopened but nobody typed in) leaves no row behind. Reads (mem_search,
mem_context, mem_doctor, …) never create one either. Engram enforces a foreign key
from observations and prompts to the session row, so the row is created and awaited
strictly before the first attributed write; a failed attempt is retried on the next
write rather than being remembered as success.
Development
pnpm install
pnpm run typecheck
pnpm run buildFor development, install by directory (see Installation) so node_modules
links to your working tree; the published package would need a reinstall on every change.
Live reload
With module HMR enabled, pnpm run build hot-reloads the running harness — no
restart. Three things about it are non-obvious and cost real debugging time:
- The
hmrrow must be enabled at boot.dsh-baseships itdisabled: true, so at boot the launcher creates a config-only fallback withroot: []that then owns thehmrservice. Enabling the row later through a live profile patch cannot add module roots to that instance — it takes one restart.patchReload: livealone only reloads config files. - The build must not clean.
cordis-plugin-hmracts only onchangeevents (if (kind !== 'change') return). A cleaning build deletesdist/first, so the watcher seesunlink+addand never reloads. That is whytsdown.config.tssetsclean: false. - A reload gives the plugin a fresh, empty session registry while agents
keep running, and no
agent/session-startfires again. Every tool therefore re-initialises its session lazily rather than assumingagent/session-startalready ran — and the prompt/passive capture listeners rebuild the state from the agent carried on their own event instead of skipping when it is missing. Bailing out there silently disabled all memory capture until the model happened to call amem_*tool.
Profile patch:
- id: hmr
disabled: false
config:
root:
- /path/to/dsh-gentle-engram/dist
ignored:
- '**/node_modules'
debounce: 100License
MIT
