Memory
dsh-engram
skepsun/dsh-engram · ★ 9
Zero-LLM auto-capture and a symbolic \[ENGRAM\] index with progressive disclosure, an ESR-lite task/evidence protocol (esr_task / esr_node / esr_close / esr_link), and per-workspace usage telemetry with an offline recall benchmark (npm run eval).
安装
dsh plugin --profile web add github:skepsun/dsh-engramGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- Why another memory plugin?
- Security model
- Data contract: export / import
- Compatibility
- Where the API family broke — 0.1.2-alpha.2
- Current release — 0.7.2
- Previous release — 0.7.1
- Previous release — 0.7.0
- Install
- Dependencies for link: installs
- What you get in the GUI
- Tools
- GC: two planes — memory-panel reclamation + Context GC (replacing auto-compact)
- Memory-panel reclamation (store maintenance)
- Context GC (replaces DSH's auto-compact)
Minimalist long-term memory for DeepSeek Harness, distilled from the symbolic-index and pi-esr ideas — with one goal: save tokens.
- Zero-LLM intake — auto-captures meaningful events from tool results by pure
pattern matching (git milestones with a written
-mcommit message, edits to key files, repeated errors), plus an explicitengram_store. Nothing on the hot path calls a model, and pure plumbing —git push/git stash/ silent commits — is deliberately never recorded (see Auto-capture policy below). Every write is run through a deterministic secret redactor first — API keys, JWTs,Bearertokens, private keys, AWS/Stripe/Slack/GitHub tokens andkey=valuesecret shapes are replaced with<REDACTED:…>markers before the dedup hash, the char cap, and storage, so sensitive text can never land on disk. Failing test runs are recognized by pure patterns (npm test,node --test, vitest/pytest/jest, failure lines) and captured astags:["error","test"]entries with a raised signal. DSH goal-domain integration: a completed or blocked goal (goal/changesession event) auto-sediments as ahandoff/errormemory (tag:goal), so the outcome of a goal survives its session (see theGET /api/dsh-engram/goalsread surface). - User-correction / preference learning (
autoCapturePreferences, default on, zero LLM) — the lesson that "when the AI makes a mistake and gets corrected, that lesson should persist" (correx, PAHF, Mem0 preference extraction): asession/eventlistener watchesuser/messageand turns deterministic correction/preference cues into a dedicatedpreferencememory kind — Chinese (不要用X,改用X,以后统一用X…) and English (don't use X,switch to X,prefer X,never use X…) patterns, three directions (avoid/switch-to/prefer), with interrogative/one-shot-instruction/ memory-tool-meta-talk filters to keep false positives out.preferencememories outrank every other kind in the[ENGRAM]block ([R]prefix) so a standing user rule is the first line the model sees each session; per-session cap + recent-window dedup mirror the auto-capture discipline. Zero LLM, zero embeddings, deterministic and pure — extracted clauses are bounded, and every write still goes through the secret redactor. - Symbolic index + progressive disclosure — a compact
[ENGRAM]block (default budget 700 chars ≈ 175 tokens; one line per memory) is injected at prompt assembly and frozen per session, keeping the request prefix byte-stable for KV-cache reuse. The agent drills down withengram_recall/engram_detailinstead of dumping raw hits into context; recall ranks the in-domain pool with an in-process BM25 pass (TF·IDF with label/phrase boosts plus a gentle time-decay factor — deterministic, zero deps), appends a compact entity-neighborhood table (reusing esr_link:node --rel--> node · conf%) for entity-anchored hits, re-warms the olderrorentry when a new failure is near-identical (repeat-failure revival: recency + hit climb toward promoteHits so the failure resurfaces instead of piling up), and on zero local hits falls back to DSH's own cross-session full-text index (ctx.sessionQuery, filtered by cwd) instead of building a parallel SQLite index; when the FTS fallback also comes back empty and the query contains CJK characters (Chinese FTS5 tokenization), recall runs a bounded substring scan over the newest session logs (zstd-decompressed, LRU-cached) and appends matched past sessions — deterministic# past sessionslines, capped files and bytes. - Session-start auto-recall (
autoRecallOnStart, default on) — the pull-only design had a gap proven in real use: recall tools relied on model initiative, and in practice the model almost never callsengram_recallon its own (19 real sessions, thousands of tool calls, ~5 recall calls total). So at the FIRST assembly of a new session the host runs one deterministic BM25 recall keyed on the session's first user message and injects the top ≤autoRecallLimithits as a[RECALL]block (default 700-char budget, "few and precise" — agentmemory candidate ①). Relevant memory is in context even without a single recall call. Pure rule, zero LLM, pure read (never bumpshits); superseded stale truth does not occupy an injection slot (still fetchable viaengram_detail); frozen per session together with[ENGRAM]for prefix stability. - Read-path activation (D) — a hit whose one-line preview is truncated
(>140 chars) carries a
…marker and the block's trailing guidance names that concrete id:#<id> truncated — engram_detail <id> (full) · engram_recall <query>. So the model knows the full record is one deterministicengram_detailcall away instead of assuming the preview is the whole memory (proven gap in real use: 66 sessions, zeroengram_detailcalls — the detail path was invisible). - Memory-to-memory semantics —
engram_storeaccepts optionalsupersedes/contradictsmemory ids (validated against the same workspace). A superseded ("stale truth") memory is demoted to the tail of recall and excluded from the[ENGRAM]block, while the superseding statement keeps the line; a contradicted memory stays ranked but is flagged· contradicted by <id>. Old rows are never deleted — re-fetchable, just honestly ranked. Opt-inautoSupersedeconfig (default off) auto-marks entity-anchored replacement updates ("改用 / no longer / switched…") as superseding their matching older row; explicitsupersedesalways wins. - Failure→fix closure (zero LLM) — error memories carry a
cmd:signature tag; when that same command later succeeds, engram auto-sediments aprocedurememory (fixed: <cmd> — N earlier failing runs now succeed), tags the earlier error rowsresolved(never re-ranking them), and recall then surfaces the fix instead of the stale failure. - ESR-lite closure protocol —
esr_task/esr_close/esr_linkgive tasks adraft → active → stablelifecycle wherestablerequires real evidence (artifact/evaluation/memory_ref), surfacing closure gaps instead of letting the agent declare victory without proof. WithverifyArtifacton (default), a non-URL artifact is resolved against the workspace (= the session cwd) and must exist on disk — otherwise the task stays ACTIVE with the reason;force:true(or disabling the toggle) skips the disk check, the three gates are still mandatory. The tool and the web forms share one gate (store.evidenceGate), so the two surfaces can never drift. Tasks are never isolated graph nodes:esr_task(entity=…)one-call wiring auto-creates theent_<slug>node and hangs the task on it (task --relates_to--> entity, idempotent), so a task created with a domain anchor immediately shows a relation; andesr_depwrites the dependency edge into both the task'sdeps(blocker logic) and the shared links table, making every task dependency a first-class visible edge in the graph. - ESR trigger mechanism (pi-esr-aligned, hybrid: static protocol + frozen
snapshot + monotonically accumulated actionables + pull) — prefix-cache
stability is the point. Teaching the model WHEN to use ESR is a static
methodology section (byte-identical every turn); the
[ESR]block is a deterministic per-session frozen snapshot (set at session start, deterministically sorted, timestamps excluded, explicitly "WILL NOT auto-refresh") plus the session's monotonically accumulated actionables: thepromote:/root-cause:/close:/stale:/escalate:lines are APPENDED exactly once, the moment each matures, and stay frozen for the rest of the session (never removed) — so the prefix changes only when genuinely new decision-point info appears, never per-turn, while the timely nudges (narrow funnel / recurring failure / work done / stale / balance) stay in context. Live state is still pulled viaesr_status(full deterministic view +since_revisionshort "unchanged" response + derived actionables). Thetools/resultrecorder feeds the actionables' data sources; P4 conversion metering stays (#suggest-*tags + 10-minute attribution +GET /api/dsh-engram/triggerstats). Pure rules, zero LLM. - Session-end todo auto-sink (draft safety net,
autoSinkTodosOnEnddefault on) — during a session the native todo stays cheap, per-session working memory that dies with the session. But when a session closes at its teardown edge (session/disposed) while its plan still has pending todos, they are auto-landed as ESRdrafttasks (name = the todo text, description marked 「源自会话计划」, deduped against existing task names, capped bymaxTasksPerWorkspace), so the plan never silently evaporates. Sinking to draft — not active — keeps them out of the[ESR]active rows and creates no evidence obligation; an explicitesr_claim/esr_taskstep is still what promotes a draft to active. The "nothing auto-written during the session" trade-off is unchanged; only the end-of-session data-loss gap is closed. Disable via profile patch or the settings card. - Memory GC (pi-esr constraints) — a scheduled, mechanical, archive-only
sweep: TTL-expired memories are archived, over-cap workspaces evict the
lowest-value entries, stable tasks past their retention window leave the
[ESR]surface, and dangling link edges are dropped. The working set (active task refs, task memories, indexed hits) is never touched, and nothing is hard-deleted — every archived entry keeps its id and stays re-fetchable. - Context GC (auto-GC = replaces DSH's auto-compact) — NOT the memory-panel
GC. It takes over the host's
compactionservice and swaps DSH's lossy LLM-summary compression for mechanical eviction + re-fetch pointers: evicted turns are scanned for engram/ESR anchors (#memory-id,tsk_*,ent_*,file_path) and replaced with one line "this detail lives atengram_detail/engram_recall/ the [ESR] block"; only un-provenanced turns get a scoped LLM narrative (gcNarrative, default on, off = fully mechanical). All six GC constraints hold (working-set-protected, pointer-salience, no-provenance-no-evict…); any error falls back to the default compressor — compaction is never broken. - Web viewer — a memory browser with benchmark-ish stats and a config card, built entirely on DSH's native settings slots (no third-party UI package).
MIT · node >= 22.19 · host-half + browser-half in one package
Why another memory plugin?
Surveys of the existing DSH plugin ecosystem show the recall-bridge, approval-gate, LLM-distillation and vector/graph niches are already crowded. dsh-engram fills the three gaps that matter for token discipline:
- No model in the write path — capture is deterministic pattern matching.
- No raw text in the prompt — a bounded symbolic index is injected, plus the top ≤ N memories auto-recalled for the session's first message (within budget); deeper retrieval stays on demand ("retrieved ≠ full-text injected").
- Honest task closure — STABLE cannot be declared without evidence.
DSH already provides cross-session FTS (ctx.sessionQuery), storage
(ctx.storageDomain), prompt-injection hooks and settings slots; dsh-engram is a
thin composition layer over them, not a re-implementation.
Security model
The DSH plugin ecosystem is young and unvetted — there is no official directory, no signature check, and the harness's read/write permission tiers do not constrain what a plugin's own code can do. So the trust bar for a memory plugin is higher than for a tool: a memory plugin sits on your prompt prefix and writes to disk on your behalf. dsh-engram's model, stated plainly:
- No network, ever. The plugin has no outbound socket, no telemetry, no update pinger. Nothing here can leak your sessions off the machine.
- No shell, no filesystem access beyond one storage file. It never executes
commands and never touches arbitrary paths. All data lives in the single
storage-domain unit
~/.dsh/storages/dsh_engram.json(plus the session-log reads it makes through DSH's ownctx.sessionQuery). - Secrets are redacted before they can land on disk. Every write path —
engram_store, auto-capture, and the import/restore route — runs text through the deterministicredactTextrule engine (API keys, JWTs,Bearertokens, private keys,key=valuesecret shapes) before the dedup hash, the char cap, and storage. Nothing sensitive is ever persisted, so nothing sensitive can be recalled later. - Doing harm requires your harness's own permissions. The plugin only asks
the host to do things via the same seams the tools you already trust use. It
does not widen sandbox policy or escalate to
danger-full-access— if you keep your profile's defaults, engram inherits exactly those bounds. - Read-mostly web API, loopback-fenced. The GUI routes under
/api/dsh-engramreject any non-loopback caller unless you explicitly whitelist a hostname viatrustedHosts; even then the same-origin fence holds. - Verifiable.
npm run dsh-engram -- doctor(/scripts/dsh-engram.mjs doctor) reports what the plugin is configured to touch; the whole store is inspectable, nothing is opaque. If you ever doubt a claim in this section, the code to check islib/redact.js,lib/store.jsand this plugin'scordis.patch.yml.
What this deliberately does not claim: it is not an air-gap. If your profile
grants the agent danger-full-access or a shell, the agent can use those —
engram is a memory layer, not a sandbox. The point above is that dsh-engram
itself adds no surface of its own.
Data contract: export / import
Memory should not be a hostage. The four tables (memories / tasks / links / entities) can be dumped and restored through a stable, versioned, machine-readable shape — so the corpus survives a reinstall, a machine move, or (when a thin adapter is written) a move to another harness, MCP end-point or Claude Code skill. The contract is plain JSON with a self-describing header:
GET /api/dsh-engram/export?workspace=/path/to/project # one workspace
GET /api/dsh-engram/export # every workspace
{
"meta": { "format": "dsh-engram/export", "version": 1, "exportedAt": 1724…, "workspace": "/path/to/project" },
"memories": [ /* all rows, archived included — a backup never loses provenance */ ],
"tasks": [ /* draft/active/stable + archived */ ],
"links": [ /* typed edges */ ],
"entities": [ /* graph nodes */ ]
}
Restore is idempotent by default and gated when destructive:
POST /api/dsh-engram/import # body: { payload, mode, dryRun?, workspace?, confirm? }
mode: "merge"(default) — writes only rows whoseidis absent, so a backup can be re-applied any number of times without duplicating anything. Each row keeps its ownworkspace.mode: "replace"— wipes the targetworkspacefrom all four tables, then restores only its rows. Destructive, so it additionally requiresconfirm: "restore"in the body.dryRun: true— computes the identical plan (what would be written / skipped) without writing a single row.- Import honors the same invariants as
storeMemory: memory text passes the secret redactor before touching disk, and rows that would break the contract (missing id/workspace, empty text, overmaxMemoryChars, id already present in merge, over the workspace memory cap) are skipped and listed in the response report instead of aborting the batch.
A restore round-trip for one workspace is then:
curl -s "http://127.0.0.1:3080/api/dsh-engram/export?workspace=$PWD" -o engram-backup.json
curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
-H "content-type: application/json" \
-d "{\"payload\": $(cat engram-backup.json), \"mode\": \"merge\"}"
Compatibility
- DSH 版本 / DSH versions:
>=0.1.2-alpha.2 <0.3.0-0(本版0.9.0; 已发布的0.3.7、0.4.0仍声明旧的0.1.x范围>=0.1.2-alpha.2 <0.2.0-0— 见下方矩阵) - 最后验证日期 / Last verified: 2026-10-27 — DSH
0.2.1-rc.2(dsh --version), web profile live composition; npm test 483/483 - 安装方式已验证 / Verified install:
dsh plugin --profile web add dsh-engram
Short version. Every published release (
0.2.0…0.3.6) supports the old DSH API graph (0.1.0-rc.7/0.1.1, peers^0.1.0-rc.7) only. DSH0.1.2-alpha.2and newer are not compatible with those releases — the settings/client API family broke there. New-DSH support starts with0.3.7, the first release of the new API generation (described below).
| dsh-engram release | Status | DSH support | Declared via |
|---|---|---|---|
0.2.0 … 0.3.6 (npm, GitHub v0.3.3–v0.3.6) |
published | old DSH only — 0.1.0-rc.7 / 0.1.0-rc.8 / 0.1.1-rc.1 / 0.1.1-rc.2 |
peers @deepseek-ai/dsh-*@^0.1.0-rc.7 |
0.3.7 / 0.4.0 (npm) |
published | new DSH, settings installSection — >=0.1.2-alpha.2 <0.2.0-0 |
engines.dsh + peers >=0.1.2-alpha.2 <0.2.0-0 |
0.5.0 (npm) |
published | DSH 0.1.2-alpha.2 … <0.3.0-0 — 设置卡迁移到 entry-id volatile Config(0.1.7+/0.2.x),≤0.1.5 自动回退 installSection |
engines.dsh + peers >=0.1.2-alpha.2 <0.3.0-0 |
0.5.1 |
published | 同 0.5.0 — 会话扫描修复(session.v<N>.jsonl.zstd,issue #8)、数据导出/导入 GUI、替换/冲突可视化(issue #3) |
同 0.5.0 |
0.6.0 |
published | 同 0.5.0 — Context Ledger、BoundaryPrune/ResultPack 默认开 + 账本否决、JIT 推送、Context GC 改 opt-in |
同 0.5.0 |
0.7.0 |
published | 同 0.5.0 — session episode、resume brief、entity pages/cards、revalidatable anchors + layered TTL |
同 0.5.0 |
0.7.1 (npm) |
published | 同 0.5.0 — web-plane Context GC on DSH 0.2.0 declarative presets (14-space rows), bundle patch re-enables the host tool-result-pruner, compaction savings feed the Context Ledger |
同 0.5.0 |
0.7.2 (npm) |
published | 同 0.5.0 — L1 boundary prune arms through ctx.inject, fixing the boot-ordering race where a pruner activating one tick after the plugin stayed dormant for the whole process |
同 0.5.0 |
0.7.3 |
未发布 | 设置卡迁到「Engram 记忆 → 设置」tab(DSH 0.2.1 删了旧 section)——随 0.7.4 一起发布 |
同 0.5.0 |
0.7.4 (npm) |
published | 同 0.5.0 — 修「推送腿」在 DSH 0.2.x 上整条静默失效([RECALL] 启动召回、JIT 推送) |
同 0.5.0 |
0.7.5–0.7.9 |
本地 | 同 0.5.0 — 账本按会话落行 + gate-audit;L1 三个真机 bug:还款闸门工作区键(Users-x-proj vs /Users/x/proj)、session.append 重入、归因不对称;L1 从「自己剪」转成「记账 + 归因」 |
同 0.5.0 |
0.7.10–0.7.12 |
本地 | 同 0.5.0 — 归因对称(宿主剪枝不进我们的 rewrite 账)、boundaryPrune 默认关闭改 opt-in、status 打印「账本两条腿」让开关判断可复核 |
同 0.5.0 |
0.7.13 |
本地 | 同 0.5.0 — 压缩入账读真实事件形状(event.data,此前 82 个 GC checkpoint 全部未入账 + 29.6M 字符)、会话计数重启不再清零(delta 累加)、CLI 尊重 DSH_HOME |
同 0.5.0 |
0.8.0–0.8.2 |
本地 | 同 0.5.0 — 证据化 procedure(v0.8-A)、导出为 DSH Skill(v0.8-B)、subagent 交接胶囊(v0.8-C) |
同 0.5.0 |
0.8.3–0.8.4 |
本地 | 同 0.5.0 — 审批门(v0.8-D)+ 其 GUI;G6 门槛改单边(Δ ≥ −2pp)+ 隐藏规格语料 |
同 0.5.0 |
0.8.5 |
本地 | 同 0.5.0 — 记忆行文件信号(⚠file-changed / ⚠file-missing),G6 第三/四轮(撤回 +30pp,Δ=+58.3pp) |
同 0.5.0 |
0.8.6 (npm) |
published | 同 0.5.0 — 修 issue #9:宿主没有 sessionQuery/snapshotEvents 时退回 seq+eventAt 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp |
同 0.5.0 |
0.8.7 (npm) |
published | 同 0.5.0 — 推送腿落盘($DSH_HOME/engram/push-legs.jsonl)+ status 打印事件日志读法;minimal 预设不再承诺永远不会发生的自动接管 |
同 0.5.0 |
0.8.8 |
本地 | 同 0.5.0 — 修推送腿遥测写错目录(engram/engram/)导致状态显示「还没记录」;pushLegsPath 契约写死在一处 |
同 0.5.0 |
0.9.0 (本期 / this release) |
本地(tag,未发 npm) | 同 0.5.0 — 产品代码仅一条 CLI 修复(--presets-dir 优先);主体是评测:G6 网络围栏(per-uid 出网白名单 + 两条前置探针)、密钥语料第 4 条自证、污染三分类与重算、G7 实测 0.2956%。不是 1.0:宿主仍 rc、G1 未测、减法债未还 |
同 0.5.0 |
Where the API family broke — 0.1.2-alpha.2
0.1.2-alpha.2 is the first version of the new API family and the first that
breaks the released plugin versions:
@deepseek-ai/dsh-settingsremovedsettingsNamespace()andinstallSettingsSection()— the exact imports every release ≤0.3.6uses — and replaced them withsettings.installSection. The old host half fails to load on0.1.2-alpha.2+.- The web client half swapped its inject set:
@deepseek-ai/dsh-client-runtimeno longer exists in the new family (replaced bydsh-api-remotesplusdsh-client-locale/-conversation/-renderer/-ui-settings-plugins), and the settings card used to talk throughctx.remote.settings. - Supporting bumps at the same boundary:
@deepseek-ai/cordis^4.0.1 → ^4.0.2,@deepseek-ai/schemastery^3.18.1 → ^3.18.2, and@deepseek-ai/dshstopped shippingconfig/agent-presetsin the package (moved todsh.configTrees).
The web config card no longer depends on any of this: since 0.3.7 it reads
and writes the dsh-engram settings namespace through the plugin's own
HTTP route (/api/dsh-engram/settings, same-origin fetch like the memory
viewer), so the card is generation-agnostic on both sides of the
0.1.2-alpha.2 boundary.
One more settings break, at 0.1.7 / 0.2.0 — and the reason 0.4.0 was
not actually loadable on the current DSH: @deepseek-ai/dsh-settings removed
settings.installSection again in 0.1.7-alpha.2 (and keeps it removed in
0.2.x). The new settings model is entry-keyed: editable fields are
.volatile() fields of the plugin's own Config (validated + hot-committed by
the loader), and the settings form is keyed by the profile entry id (engram),
so nothing needs "mounting". 0.5.0 feature-detects the two generations in
installEngramSettings:
- DSH
≥0.1.7/0.2.x— the 34 GUI knobs are the volatileConfigfields (lib/settings.jsmakeEngineConfigSchema, exported asConfigfromlib/index.js); the card reads/writes entryengramthrough the same/api/dsh-engram/settingsHTTP route, and theservice.settingsseam re-syncsresolvedonloader/volatile-updateso card edits land live on new sessions. - DSH
≤0.1.5— the legacysettings.installSectionnamespace (dsh-engram) is registered as before; nothing changes.
The declared range >=0.1.2-alpha.2 <0.3.0-0 covers both, and the HTTP wire
namespace stays dsh-engram on both generations (the seam's active namespace
drives mutations).
So: keep DSH on 0.1.1-rc.2 or older while running a release ≤ 0.3.6, and
upgrade the plugin to >=0.3.7 before moving DSH to 0.1.2-alpha.2.
Current release — 0.7.2
L1 boundary prune was silently dormant on some profiles — a boot-ordering race, not a missing patch.
BoundaryPrune resolved DSH's toolResultPruner with a strict
ctx.get("toolResultPruner") at install time. On DSH 0.2.0 web profiles the
pruner's fiber activates ~100 ms after the plugin (measured on the live
composition dsh-context + dshmarket + dsh-engram: t0 strict=undefined / loose=object, strict resolves 100 ms later), so the strict lookup was empty,
installBoundaryPrune gave up, and L1 never armed for the life of the process —
taking the ledger's only prune-credit leg with it. The status snapshot also
reported that race as "row missing", pointing at the wrong fix.
- Arm through
ctx.inject(["toolResultPruner"], …)— cordis runs the callback the moment the service becomes active (and again after a reload), so arming no longer depends on composition order; the durable status snapshot is rewritten as soon as L1 is armed. - "Absent" and "late" now read differently —
boundaryPruneAvailabilityfalls back to a non-strict lookup: a disabled/missing row still saysno toolResultPruner service … (the bundle patch must re-enable …), a registered-but-inactive one saysregistered but its fiber is not active yet. - Verified — the live bundle composition (with
dsh-context) reproducesarmed:falsein an isolatedDSH_HOME; after the fix the same composition reportsarmed:true · threshold 8192.npm test406/406.
Previous release — 0.7.1
Web-plane Context GC on DSH 0.2.0's declarative presets — the preset
format changed shape (one @deepseek-ai/dsh-agent-preset declaration inside
dsh-web-app/presets/*.patch.yml), and the auto-wiring was anchored to the old
agent.cordis.yml layout, so autoWebCompaction silently skipped every preset
(and could half-rewrite a stock row: the pattern did not include its trailing
newline).
- Structural row location — the
compactiongroup is found by YAML structure (a list item carryinggroup: true), not by column position; the stock/engram rows are validated by BOTH their- id:andname:values, and the replacement inherits the row's own indent. Legacyagent.cordis.yml(4 spaces) and declarative patch files (14/16/18 spaces) both round-trip byte-identically. - Declarative discovery — the preset roster no longer carries
pathon DSH0.2.0; the plugin maps each roster id to the shipped<presetsDir>/<id>.patch.yml(resolved through thedshinstall, withDSH_WEB_PRESETS_DIRoverride) and skips user-defined declarative presets (profile-patch overrides) with an explicit log line instead of failing. - Status/doctor —
npx dsh-engram status/doctordiscover both preset generations, show the detected layout, and flagautoWebCompaction=false(the opt-in toggle that makes the web plane a no-op), node_modules preset wiring (survivesnpm update dshonly while auto-wiring stays on), and profile-patchpreset-*overrides that outrank the shipped files. - Migration — restart
dsh webwithautoWebCompaction:true: the next boot re-wires every stock declarative preset (idempotent refresh; existing hand-wired rows are rewritten to the requested engine config).npm test396/396.
Previous release — 0.7.0
From event records to maintainable current truth: sessions sediment an
episode at teardown, new sessions get a resume brief, esr_node becomes a
readable page, and file anchors make stale truth detectable.
- Session episode — zero-LLM digest at
session/disposed(files / commands / failures→fixed / decisions / open loops), never indexed, newest 20 per workspace kept. - Resume brief — the newest episode from the last 7 days is injected as a
[RESUME]line (≤240 chars + drill pointer) at session start. - Entity pages + cards —
truth / timeline / open loops / conflicts / relations; up to 2 one-line cards in the index, full page viaengram_detail(entity=…). - Revalidatable anchors — file-backed memories record
mtime/size/sha256; a bounded revalidation at session start tags changed/missing sourcesstale(dropped from index/truth, still re-fetchable). - Layered TTL — preference/procedure permanent, episode 14d, error 60d, others 180d.
- Migration — only new writes are anchored (no historical backfill);
restart
dsh webafter upgrading; an episode appears after one session ends, the resume brief on the next new session. The0.6.0ledger / defaults / JIT behaviour is unchanged.
The published 0.4.0 declared >=0.1.2-alpha.2 <0.2.0-0 — which the semver
gate rejects on every DSH 0.2.0-0 prerelease (<0.2.0-0 excludes
0.2.0-rc.x), and its host settings half still called the installSection
API that dsh-settings 0.1.7+ removed. 0.5.0 fixes both: the range
opens to <0.3.0-0 (verified against DSH's own includePrerelease check, so
0.2.0-rc.2 and the whole 0.2.x line pass), and the settings card is
migrated onto the entry-id volatile Config model described above (with a
feature-detected legacy fallback).
| Component | Supported range |
|---|---|
| DSH runtime / host / client package family | >=0.1.2-alpha.2 <0.3.0-0 |
@deepseek-ai/cordis |
^4.0.2 |
@deepseek-ai/schemastery |
^3.18.2 |
| Node.js | >=22.19.0 |
0.1.2-alpha.2 is the minimum version for this API family: host settings are
feature-detected — on DSH ≤0.1.5 they register the dsh-engram
namespace via settings.installSection; on ≥0.1.7 / 0.2.x the form is the
plugin's Config with the 34 GUI knobs marked .volatile() (entry id
engram). The browser settings card reads and writes through the plugin's own
HTTP route (/api/dsh-engram/settings, see Route B above) on both. The full
regression suite (npm test 396/396, incl. the new settings-v2 coverage for the
volatile-Config model) passes against the DSH 0.2.0-rc.2 dependency set
(cordis 4.0.4 + schemastery 3.18.4 + cosmokit, matching the global DSH CLI).
The .volatile()/loader hot-commit path and the describe-side projection were
verified against the real dsh-settings 0.2.0-rc.2 utilities, and a full
real-machine boot smoke on an isolated dsh 0.2.0-rc.2 profile passed:
the plugin boots with no version-range denial, the settings card form is
served for entry engram (all 27 GUI knobs, incl. trustedHosts), and a
GET → PUT (mutate ops, revision 0→1) → unset round trip on
/api/dsh-engram/settings lands in the profile row and reads back. The older
0.1.0-rc.7 and 0.1.1 API graphs are outside this release's compatibility
range.
To install the DSH version used for validation:
npm install --global @deepseek-ai/dsh@0.2.0-rc.2
dsh --version
Install
# from GitHub (this repo)
dsh plugin --profile web add github:skepsun/dsh-engram
# once published to npm
dsh plugin --profile web add dsh-engram
# local development (symlink — edits apply immediately)
dsh plugin --profile web add link:/path/to/dsh-engram
Then restart dsh web. Data persists in ~/.dsh/storages/dsh_engram.json.
The npm and GitHub installs below need no manual dependency step: pnpm installs
zodand vendors the optional@deepseek-ai/*peers into the plugin's ownnode_modules, and the CLI auto-registers the plugin into the profile'sdsh.profile.bundles. Thesetup-linksstep below is only for thelink:development workflow, where pnpm deliberately does not install a symlinked directory's dependencies.
A fresh session is required to see the injected
[ENGRAM]/[ESR]blocks and the tools; both prompts and the tools registry are assembled per session.
Dependencies for link: installs
A symlinked plugin resolves its imports from its own node_modules, so the
host-side dependencies must be present next to the checkout — they are not
tracked by git:
cd /path/to/dsh-engram
node scripts/setup-links.mjs # one command: links the @deepseek-ai
# workspace packages into node_modules AND
# installs zod (reused from the harness
# pnpm store, or via `npm install`)
The script auto-locates the harness checkout at ../deepseek-harness (also
works when it sits next to the repo's parent, e.g. E:\deepseek-harness +
E:\kototoro_demo\dsh-engram); override with DSH_HARNESS_DIR. Without this
step, dsh web boot fails with ERR_MODULE_NOT_FOUND: Cannot find package 'zod' (and would fail on the @deepseek-ai/* peers next). node scripts/setup-links.mjs --check prints the state without writing anything.
What you get in the GUI
After restart, inside the native DSH settings surface:
Sidebar "ESR 看板" entry + full-screen kanban — one more row under New Session with a live active-task badge (polled from /overview every 30s across all workspaces). Clicking it opens a full-screen board in the center column: 草稿 / 进行中(gaps) / 就绪(evidence ready) / 已闭环 columns, workspace filter + search, an inline create form, and per-card "补齐证据 → 关闭" closure forms sharing the esr_close gates (artifact + evaluation + memory_refs). A 看板 / 图谱 (board/graph) toggle in the header reuses the full relation graph (esr_node/esr_link force-directed SVG: entity circles + task check badges, drag/zoom/select for relation details), following the workspace filter and refreshed by the same 20s polling. Following the task-board precedent, the entry and the board are DOM-mounted and self-heal (MutationObserver re-inserts on shell re-renders), with cross-panel exclusivity against task-board/ssh (opening one evicts the others; clicking a sidebar session/workspace row hands the center column back to the conversation). The conversation subtree stays mounted underneath and is hidden by
html[data-dsh-engram-board-active], so toggling loses no state.Unified task strip above the composer — the conversation dock that DSH ships for its built-in todo tool is taken over (same
conversation.input.dockcell /id: todoat a lower priority) and merged into one modern control: the session's current plan (todo_write'stodosprojection) plus the workspace's persistent ESR tasks (with evidence-gap badges and an inline "补齐证据 → close" form) plus the relation graph rendered as node → relation → node chips with entity/task names resolved. It only shows while there is something to show, stays live with 15s polling, and the built-in plan still renders (without the ESR parts) if the loopback-fenced API is unreachable. A workspace-switcher chip leads the strip: it defaults to following the current session (the tooltip says so), and its dropdown pins the ESR task/relation source to any workspace (✓ marks the active pin; the × or "follow session" entry reverts). Switching refetches immediately and is a pure UI focus change — the model's session context and the per-session frozen injection blocks are untouched.Settings → Engram Memory — a standalone first-class settings section (right after the Plugins section, not a child tab of it). Default "All workspaces" view shows every workspace's memories/tasks/links grouped by workspace (dropdown + prev/next workspace pager; the memory table additionally pages 10 rows per page with a jump dropdown, fixed column widths, 3-line clamped content ellipsis and full text on hover). Overview stat cards (counts by workspace/kind, auto-capture totals, per-workspace
[ENGRAM]index token estimate, cumulative GC totals), a searchable / filterable memory table with archive + delete actions, an ESR task board with an inline "new task" form and a per-task "fill evidence to close" (artifact / evaluation / memory_ref → STABLE, same gates as esr_close), a node + relation list (nodes are domain objects the model registers via esr_node — package/service/repo/concept; relations via esr_link), a separate relation-graph tab (hand-rolled force-directed SVG, no chart library so the bundle stays pure: entities as circles, tasks as check badges, relations colored per type with direction arrows; drag nodes, pan, wheel-zoom, re-layout, hover highlights the neighborhood, clicking a node pops a floating panel with its incident relations and linked objects; dangling links are counted and warned about), an injection-preview tab that renders the exact[ENGRAM]index block (order 40) and[ESR]task/closure block (order 41) the model sees each session — same pure functions as the system-prompt sections — as two terminal-style panes with per-line coloring (block headers, task lines, drill hint, and the data-drivenescalate:reminder highlighted), line/char/~token cost chips plus memory/task/link/node count chips, 20s auto-refresh and one-click copy of the raw block text (backed by the newGET /api/dsh-engram/preview?workspace=…route). Every ESR task card (board and ESR tab) carries an evidence-progress ring — a small three-arc SVG donut mapping artifact · evaluation · memory_ref, all green when closure-ready, amber while gapped, gray with no evidence yet; the board header adds an aggregate ring showing overall evidence completeness (%) plus how many in-progress tasks are closure-ready. Pure SVG, no chart library, bundle stays clean. A telemetry dashboard tab turns the /stats usage rollup (workspace × day) into a pure-SVG dashboard: three gauges for ESR proactivity (benchmarked against the 0.34 escalate threshold, amber + hint when low), recall hit rate and detail follow-through, five stat cards (total / esr / memory calls, avg hits per query, failures), a 14-day mem-vs-esr stacked bar chart and a Top-8 tool breakdown (mem blue / esr purple), 20s auto-refresh with an automatic small-sample (<10 calls) warning. A details sidebar (master–detail) opens on the right when you click any task / memory / node / relation row: task cards show state badge, evidence ring, full id + timeline, gap list, clickable memory references and an inline "fill evidence to close" form that refreshes the lists on success; memory cards show full text, tags, signal/hits/TTL and provenance metadata; node cards list all incident relations (typed, colored, with direction + confidence); clicking a task's memory reference jumps straight to that memory, with a hint when it is not in the loaded set. And a memory-GC panel (dry-run toggle + run button + pointer report). The GUI create/close use the host's newPOST /api/dsh-engram/tasksandPOST /api/dsh-engram/tasks/closeroutes. Model-side proactivity is driven by the [ENGRAM]/[ESR] injected blocks: multi-step work gets a task now, recurring domain objects get a node, related tasks/nodes get a link.Real behaviour telemetry (agent observability) — the ESR page opens with an "agent behaviour" panel fed by a new
usagetable (per workspace × day rollup) +GET /api/dsh-engram/stats: everyengram_*/esr_*tool call is recorded (counts, failures, recall mechanics). Reported ratios: ESR proactivity = esr calls / (memory + esr calls); recall hit rate = recalls returning ≥1 hit / total recalls; mean hits per query; detail conversion = a engram_detail following a hit recall within 8 session events. Per-tool counts + a 14-day daily rollup are shown too. Numbers are real, from real sessions — lift ESR proactivity by watching this panel and tuning the injected prompt.Settings → Engram Memory → 设置 tab — the config card, expanded by default (DSH 0.2.1 dropped the
settings.plugin.itemslot, so the old Settings → Plugins → Plugin configuration seat no longer renders). Same card, same transport: title + one-line description + chevron, its ~12 options under four groups — Capture & Search / Index / Lifecycle & GC / Security. Changes apply to new sessions (frozen blocks stay stable); Discard / Save with an "unsaved" badge on the header. It drives the namespace through the plugin's own/api/dsh-engram/settingsroute (not the isLoopback-gated scope), so it stays editable even through an operator-authorized tunnel.Settings → Plugins → Plugin configuration → dsh-engram (DSH ≤ 0.2.0 only, legacy
settings.plugin.itemseat) — a collapsible config card in the same style as the built-in "Shell / Agent loop / Web search" cards: title + one-line description + chevron, collapsed by default, click to expand/collapse. Open, its ~12 options render under four groups — Capture & Search / Index / Lifecycle & GC / Security. Changes apply to new sessions (frozen blocks stay stable); Discard / Save with an "unsaved" badge on the header. The card drives the namespace through the connection's own settings RPCs (not the isLoopback-gated scope), so it stays editable even when the GUI is reached through an operator-authorized tunnel.
The browser half is served by DSH's client-module loader directly from this
package (dsh.client + exports["./client"], no web-application rebuild); the
data comes from the loopback-fenced /api/dsh-engram/* route family. The fence
stays closed by default; to reach the memory viewer from an authorized tunnel
hostname, list it in the plugin's trustedHosts config (e.g. via the registry
or a profile patch):
// patch/engram.json
{ "engram": { "trustedHosts": ["cream-club-fragrances-caught.trycloudflare.com"] } }
If you change client/src, rebuild the bundle with:
npm run build:client
Tools
| Tool | Purpose | Kind |
|---|---|---|
engram_store |
Explicitly store one memory (kind, tags, optional entity anchor, optional supersedes/contradicts ids, optional source/conditions) |
write |
engram_recall |
Deterministic keyword recall over workspace memories; optional search_sessions FTS over past sessions; opt-in scope=global/recallScope cross-workspace recall with [W:<ws>] origin markers |
read |
engram_detail |
Full record of one memory id (provenance, tags, hits) | read |
esr_task |
Create a task entity (draft → active) | write |
esr_close |
Close a task via the evidence protocol (artifact + evaluation + memory_ref) | write |
esr_link |
Add a typed relation between two entities (mini graph) | write |
esr_dep |
Add a dependency edge (blocks / relates-to / parent-of) between tasks | write |
esr_claim |
Atomically claim a task (assignee + claimedAt, draft → active) | write |
esr_unclaim |
Release a claimed task's assignee | write |
esr_ready |
List claimable tasks (no open blocker, unclaimed) | read |
esr_status |
Pull live ESR state + derived hints (since_revision short "unchanged" response) |
read |
esr_node |
Create/update an entity node (stable symbol) | write |
esr_gc |
Run the memory GC for the workspace (dry_run:true previews) |
write |
esr_model |
Precomputed mental model of the workspace (brief/full, max_chars) |
read |
The [ESR] block is a frozen per-session snapshot and never auto-refreshes —
call esr_status for the live truth.
GC: two planes — memory-panel reclamation + Context GC (replacing auto-compact)
Memory-panel reclamation (store maintenance)
A scheduled sweep (gcIntervalHours, default 24h) plus a manual esr_gc /
GUI button keeps the store bounded the pi-esr way — mechanical, working-set
protected, archive-only:
- TTL-expired memories are archived (soft; the id stays, retrievable via the GUI's archived filter);
- over-cap workspaces evict the lowest-value non-protected memories;
- stable tasks past
gcStableRetentionDaysbecome archived and leave[ESR]; - links whose both endpoints are gone are dropped (dangling edges).
GC never touches the working set: memories referenced by an active task
(memory_refs), task-kind memories, and already-indexed hits
(hits >= promoteHits). Run esr_gc with dry_run: true to preview. Nothing
is hard-deleted — the report ends with re-fetch pointers for everything it
archived, so archives are recoverable, not lost.
Context GC (replaces DSH's auto-compact)
DSH's default context compression is a lossy LLM full summary
(compaction-basic): evicted history is crushed into prose — un-queryable, and
the summary itself burns tokens. dsh-engram's auto-GC replaces it:
ContextGcEngine extends BasicCompactionEngine overrides the single
summarize() hook and swaps the summary body for:
- Scan — the evicted messages are scanned for provenance anchors: memory
ids echoed by
engram_store/engram_recall/engram_detail,tsk_*/ent_*touched byesr_*, andfile_pathanchors (esr_gcand other management calls are NOT anchors); - Pointer summary — every evicted category carries an explicit re-fetch
call (
engram_detail(id: "…")/engram_recall(query)/ the [ESR] block /esr_ready); the active working set is restated, never evicted; - Narrative safety net — only un-provenanced turns (pure chat / reasoning)
get a scoped LLM summary (
gcNarrative, default on; off = the whole path is zero-LLM and un-provenanced turns are kept as truncated verbatim).
Triggers follow DSH's own compaction timing (step pressure / context-overflow /
/compact); the lock, replay validation, tool-call/result pairing and token
pricing are all reused from the basic engine. Any error falls back to the
default compressor — compaction is never broken; with gcReplacesCompaction: false (the default since v0.6, or a host without dsh-compaction-basic) a bare
BasicCompactionEngine is mounted instead, so the compaction service never
disappears while dsh-engram is installed. Mounting registers the compaction
service on the plugin's fiber, so unloading/reloading engram restores the
default engine. Taking over compaction is opt-in: turn on
gcReplacesCompaction (and autoWebCompaction for web presets).
Shrink gate: the harness refuses any checkpoint that is not strictly smaller than the evicted span (
summary is not smaller than the shadowed content) and rolls the compaction back — otherwise the context never shrinks and the session eventually hits the model-side overflow. Context GC self-predicts that gate: it mirrors the harness framing with the hosttokenMeter(verified token-for-token identical) and truncates the pointer/narrative body to the evicted span's token budget (the pointer head and working set survive; the trimmed tail stays recoverable in the session log), so compactions actually commit. Full from-scratch usage:docs/CONTEXT-GC-GUIDE.zh.md.
Where it is enabled (the entry point)
Assembly happens on two planes, but both switches are opt-in (no takeover by default):
- Host plane (headless / TUI / base profiles):
the main plugin (
lib/index.js) mounts the engine insidectx.effectviamountCompactionEngine(ctx, resolved, { readWorkspace }) → new Engine(), registering thecompactionservice on its own fiber; the plugin patch disables the basecompaction-basicrow, making engram the sole host-plane provider.gcReplacesCompaction: false(default) → a bareBasicCompactionEngine(DSH's own summarizer);true→ContextGcEngine(pointer summaries). Enable via profile patch (config: { gcReplacesCompaction: true }) or the settings card. - Preset plane (web profile) — covers every stock preset once enabled: web keeps compaction inside each agent preset's isolated realm
(the shipped presets mount their own
compaction-basic), unreachable from a host-plane row — and unreachable from a profile patch too (mountPresetbuilds itsInclude.Configwith no patches; verified against the harness source). So on boot, once theagentPresetsservice is ready, the plugin auto-edits every preset that still has the stock layout — the default preset plus the whole roster, across BOTH preset generations: legacy<root>/<id>/agent.cordis.ymlcompositions (DSH ≤ 0.1.x, row at 4 spaces) and the declarative@deepseek-ai/dsh-web-app/presets/<id>.patch.ymlfiles (DSH ≥ 0.2.0, row at 14 spaces inside the@deepseek-ai/dsh-agent-presetdeclaration; a user-defined declarative preset with no shipped file is left alone). The row is located by YAML structure (a list item carryinggroup: true) and validated by both its- id:andname:values, so the swap is column-position independent: it swaps thecompaction-basicrow inside thecompactiongroup fordsh-engram/compaction. That auto-wiring is:- off by default (opt-in) (
autoWebCompaction: false, toggle in the settings card's memory-GC section); once enabled it is idempotent — an already-wired preset is left alone; what "off" means: the plugin stops auto-wiring on later boots (the headless/TUI/base host plane is unaffected) — it does NOT un-wire presets that were already wired; usenpm run web-compaction:revertfor that; - per-preset: only stock-layout presets are touched — a preset with a
custom compaction group, or none at all (e.g. the shipped
minimalpreset), is never written, only logged; - backed up + validated: a create-only
.engram.bak(agent.cordis.yml.engram.bakor<id>.patch.yml.engram.bak) holds each original next to its file, every result is re-read and verified after writing, and any doubt rolls that file back; a failure only warns and that session keeps DSH's default summarizer — the host plane is never affected. - **config actually propag
- off by default (opt-in) (
…