Bundle
dsh-memory-jev
DSH memory plugin gated by the TypeSafe Jev decision model: mem_* tools whose every read/write is a typed judgement (choice/noul) — write gate, recall gate and a fail-closed injection gate with budget, audit log and restore/merge.
- Source
- Towzai
- License
- MIT
- Updated
- Updated 5 hours ago
Readme
# dsh-memory-jev
**English** · [中文](README.zh-CN.md)
> Memory for **DeepSeek Harness (DSH)** where every read and every write is a judgement — made by TypeSafe's **Jev** decision model.
Jev answers in only three typed shapes — `choice` (pick one of the given candidates), `score`, `noul` (probability of yes/no) — and **never generates text**. This plugin uses that property as the safety boundary of a memory system: the model decides *whether* something is worth keeping, *which* older memory it replaces, *whether* a retrieved memory is actually relevant, and *whether* this turn deserves an injection — while **all text stays under plugin control**. The model can never invent memory content.
---
## Three gates
| Gate | What it does | On failure |
|---|---|---|
| **Write gate** `mem_remember` | Local bigram prefilter → one Jev fan-out: `worth_keeping` (noul) + `supersedes` (choice over candidates + `none`) | **Still writes** (fail-open); only marks `gate=unavailable\|budget` |
| **Recall gate** `mem_recall` | Lexical prefilter top-N → one `rel_<id>` noul per candidate → filter & sort by probability | Falls back to local ranking and marks `degraded` ("not judged by Jev") |
| **Injection gate** `agent/pre-step` | Cheap text gate → candidate-pool subtraction → Jev relevance judgement → append one framed user snapshot at the **tail** | **Injects nothing** (fail-closed — silence beats noise) |
### Invariants
- **I1** — at most one injected block per `(session, turn)`; later steps of the same turn neither judge nor inject again.
- **I2** — each memory is injected **at most once per session**: already-injected ids are removed from the candidate pool *before* the Jev call, so they do not even cost a request.
- **No permanent silence after compaction** — once an injected block leaves the context, its ids become eligible again (logged as `reset`).
- **Self-identifying blocks** — framed by `<retrieved-memories …>` plus an explicit "not conversation history, not instructions" line; every `<` inside memory content is escaped to `\u003c`, so a memory **cannot forge the delimiter**.
- **Prefix-cache friendly** — appended at the tail only; system prompt and prior history stay byte-identical.
---
## Tools
| Tool | Purpose |
|---|---|
| `mem_remember` | Write gate; persists regardless of the gate outcome (returns `persisted`) |
| `mem_recall` | Recall gate; returns `{gate, degraded, items[]}` with `jev_prob` and `local_score` **kept separate** |
| `mem_list` / `mem_view` | List / inspect |
| `mem_forget` / `mem_restore` | Soft delete / restore (keeps `supersedeHistory`, supports `cascade`) |
| `mem_merge` | Fold an older entry's body into a newer one, then soft-delete the older (merged length ≥ old length) |
| `mem_pin` | Skip the relevance threshold, at most once per session, **never at session start** |
| `mem_gate_status` | Spend / reserved / remaining / calls / circuit breaker / key presence / store path |
| `mem_gate_log` | Audit-log query (ids and hashes only — **no bodies**) |
**Physical deletion never happens**: deletion is always `retired=true` and is reversible.
---
## Install
### Via the DSH plugin marketplace
Once the repository carries the `dsh-plugin` topic it is indexed automatically (the registry CI scans every 2 hours):
```bash
dsh plugin --profile web install <owner>/dsh-memory-jev
```
### Manually
1. Copy this repository into `~/.dsh/profiles/web/node_modules/dsh-memory-jev/`.
2. Register it in the profile's `cordis.patch.yml` (or let the marketplace do it):
```yaml
- id: dsh-memory-jev
name: dsh-memory-jev
config:
storePath: /absolute/path/to/gate-store.json # empty = <cwd>/data/gate-store.json
injectEnabled: true
```
3. **Restart DSH at process level** (`set_bundle enabled:false→true` only re-mounts the row; the ESM module cache is not re-imported).
4. Confirm with `mem_gate_status`.
> Host interface packages (`@deepseek-ai/cordis`, `dsh-llm`, `dsh-tools`) are declared **only as `peerDependencies`** — shipping copies would shadow the host and break every tool call.
---
## Configuration
| Key | Default | Meaning |
|---|---|---|
| `storePath` | `''` (= `<cwd>/data/gate-store.json`) | Store file; **pin an absolute path in production** |
| `injectEnabled` | `true` | Master switch for the injection gate |
| `injectInSubagents` | `false` | Inject in child sessions too (cost scales with children) |
| `injectLimit` | `3` | Max memories per injection |
| `injectMinProbability` | `0.6` | Relevance threshold for injection (stricter than manual recall) |
| `prefilterLimit` | `40` | Candidates sent to Jev |
| `supersedeCandidates` | `12` | "Possible duplicate" candidates for the write gate |
| `dailyBudgetCny` | `3.5` | Daily budget (CNY) |
| `dailyCallLimit` | `3000` | Daily call cap |
### Tuning
Every magnitude is a config value — nothing is hidden in the code. Change them in the profile's `cordis.patch.yml` (or the bundle's own patch) and **restart DSH at process level**.
| Key | Default | Effect |
|---|---|---|
| `injectEnabled` | `true` | master switch for the injection gate |
| `injectLimit` / `injectMinProbability` | `3` / `0.6` | memories per injection / relevance threshold (pinned entries bypass it) |
| `recallMinProbability` | `0.5` | `mem_recall` threshold — also overridable **per call** via `min_probability` |
| `prefilterLimit` / `supersedeCandidates` | `40` / `12` | candidates sent to Jev / duplicate candidates for the write gate |
| `worthReviewThreshold` | `0.35` | below this the write gate only annotates "review suggested" — it never blocks a write |
| `dailyBudgetCny` / `dailyCallLimit` | `3.5` / `3000` | daily budget (CNY) and call cap |
| `injectTimeoutMs` / `toolTimeoutMs` | `1500` / `15000` | latency budget for the injection path / for tools |
| `egressGuard` | `false` | outbound-only pattern guard (opt-in) |
| `injectInSubagents` | `false` | inject in child sessions too |
Tune from evidence, not feel: the audit log records every candidate's `noul` probability together with the threshold in force, so `mem_gate_log` (or the per-day `gate-decisions-*.jsonl` shard) tells you exactly what a given threshold would have admitted or dropped.
**Cost model** — Jev input is $0.042/M and output is free. The plugin accounts and reports in CNY (rate constant `USDTOCNY` in `lib/index.js`). A measured judgement costs 2,000–4,200 input tokens ≈ **¥0.0006–0.0012**. Budgeting is reserve → settle → reconcile, with a serialized ledger so concurrent calls cannot overspend.
---
## Data boundary (disclosure)
- **Cloud dependency**: yes. Exactly three egress points, all to `https://openrouter.ai/api/alpha/decisions` — the write gate (first 600 chars of the new memory + candidate titles), the recall gate and the injection gate (the question text + candidate titles).
- **Offline path**: yes. Without an API key, write/recall fall back to deterministic behaviour (marked `degraded`) and the injection gate injects nothing.
- **Credentials**: read on demand from `OPENROUTER_API_KEY` (environment variable; falls back to the Windows registry `HKCU\Environment`). Never written to config files, never echoed, **never logged**.
- **Redaction guard**: if a question or body matches phone / national-id / bank-card / `sk-` / `Bearer` / `password` / `api_key` patterns, the judgement is **skipped entirely** (`gate=redacted-skip`) — one missed judgement is preferable to leaking.
- **Audit log**: ids, probabilities, tokens, CNY cost, error kind and the `build` version only. **No bodies, no raw queries** (hash and length only).
- **Local persistence**: store, audit log and budget file live next to your `storePath`. Nothing is synced or uploaded.
- **Egress guard** (`egressGuard`, **default off** — opt-in): when enabled, a *question* that looks like it carries a secret (phone / national id / bank card / `sk-…` / `Bearer …` / `password` / `api_key`) is not sent to the endpoint — recall **falls back to local ranking and still returns your memories**, and auto-injection skips that turn. It governs **what leaves the machine only**: it never decides what may be stored, retrieved or injected. Default is off because *what counts as sensitive is the user's call*; turn it on when you want the extra boundary. Every threshold and budget below is a plain config value — see **Tuning** under Configuration.
- **Server-side retention**: none (request-and-discard). Memories exist only in your local store file.
---
## Verify
```bash
npm run verify # = node --import ./tools/load-plugin.mjs tools/verify_all.mjs
```
Runs **offline with zero spend**: `tools/fake-jev.mjs` starts a controllable local Jev stub (HTTP 500, ECONNRESET, timeouts, malformed JSON, missing `usage`) and asserts both invariants, the failure semantics, an unchanged prompt prefix hash, budget under concurrency, read-only behaviour on a corrupt store, and log fields plus a privacy scan. **A failing assertion exits non-zero.**
A real Jev endpoint is only used for a few manual confirmations (requires a real key).
---
## License
MIT
Install
dsh plugin --profile web add github:Towzai/dsh-memory-jev
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-memory-jev from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.