dsh-layered-memory 0.5.4 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md ADDED
@@ -0,0 +1,211 @@
1
+ [简体中文](README.md) | **English**
2
+
3
+ <p align="center">
4
+ <img src="./assets/readme/hero.svg" width="100%"
5
+ alt="dsh-layered-memory: conversations are automatically distilled into layered memories and injected before every model step (L0 raw conversation → L1 atomic memories → L2 scene blocks → L3 core persona)">
6
+ </p>
7
+
8
+ # dsh-layered-memory
9
+
10
+ A **layered distillation memory plugin** for DeepSeek Harness (persistent composition
11
+ plugin): conversations are processed in the background through L0 capture → L1 atomic
12
+ memories → L2 scene consolidation → L3 persona distillation, and relevant memories are
13
+ automatically injected into context before every model step — neither the user nor the
14
+ model needs to do anything. Ported from the pipeline design of
15
+ [MemoryCore](https://github.com/TencentDB-Agent-Memory) (TencentDB Agent Memory):
16
+ prompts are kept as-is; only the L2/L3 "LLM manipulates files" flow is adapted to
17
+ "LLM outputs, engineering side executes".
18
+
19
+ ## Core Capabilities
20
+
21
+ ### Layered Memory (L0–L3)
22
+
23
+ | Layer | Content | Storage |
24
+ | --- | --- | --- |
25
+ | L0 | Per-turn user/assistant messages (cleaned, code blocks and injected tags stripped) | `conversations/YYYY-MM-DD.jsonl` + SQLite |
26
+ | L1 | Scene segmentation + memory extraction (chat: persona/episodic/instruction; work: work_fact/work_task/work_method/work_artifact) + conflict-detection dedup & merge, each record family-tagged | `records/YYYY-MM-DD.jsonl` + SQLite (FTS5 + optional vectors, family column) |
27
+ | L2 | New memories consolidated into Markdown scene documents (META blocks, heat management, merge caps), consolidated per family | `scenes/chat/*.md`, `scenes/work/*.md` |
28
+ | L3 | Persona distilled from changed scenes (chat: user persona ≤2000 chars; work: Team Operating Doctrine ≤1200 chars), one per family | `persona-chat.md`, `persona-work.md` |
29
+
30
+ Pipeline principles: all distillation calls reuse DSH's own `ctx.llm`; any stage
31
+ failure is logged only and never blocks the agent loop; recall injection is wrapped
32
+ in tags that are automatically stripped on the capture side, preventing feedback loops.
33
+
34
+ ### Per-Session Memory Modes
35
+
36
+ Each session can independently choose a memory mode — **write and recall share the
37
+ same mode**:
38
+
39
+ | Mode | Distillation (write) | Recall (injection) |
40
+ | --- | --- | --- |
41
+ | `Auto` (default) | Single extraction with a merged-vocabulary prompt, all personal 3 + work 4 types enabled, family tag assigned by type prefix | Both families recalled; persona/scene navigation grouped by category and injected with structured `<domain>` blocks |
42
+ | `chat` | Narrow prompt with personal 3 types only, chat family only | chat memories only + chat persona/scene navigation |
43
+ | `work` | Narrow prompt with work 4 types only, work family only | work memories only + work persona/scene navigation |
44
+ | `Off` | No L0 writes, no distillation | No recall; the three memory tools return a "cloaked" notice |
45
+
46
+ - **Control**: the pill next to the mode selector in the input bar (`Memory · Auto`);
47
+ clicking opens a macOS-style sliding picker above — release to snap to the nearest
48
+ mode;
49
+ - **Default mode** = config `family` (`auto|chat|work`, default `auto`); each session's
50
+ choice is persisted by sessionId to `session-modes.json`, surviving restarts/session
51
+ restore; switching mid-session takes effect next turn, already-extracted memories
52
+ stay in their original family;
53
+ - Stacks with the global switches (global is the master gate); L2/L3 are fully
54
+ family-isolated — content never leaks across families.
55
+
56
+ ### Memory Browser (Settings → Memory)
57
+
58
+ Multi-tab page with a mixed view of both families: **Overview** (per-layer counts +
59
+ memory mode switch panel, auto-refresh every 5s), **Memories** (L1 card list with
60
+ keyword/type/scene filters), **Scenes** (L2 full text), **Persona** (L3 full text),
61
+ **Log** (last 200 lines of `memory.log`). Switches go through the official settings
62
+ service (namespace `dsh-memory`, effective immediately, persisted across restarts);
63
+ effective rule = **static config (deployment ceiling) AND runtime switch**; data
64
+ channel is loopback RPC (`dsh-memory/*`).
65
+
66
+ ## Getting Started
67
+
68
+ Requires Node ≥ 22.16. Two invocation styles — the `npx` prefix can replace `dsh` in
69
+ any command below:
70
+
71
+ ```bash
72
+ # Option 1: run the official CLI directly via npx (no pre-installed dsh; version can be pinned, e.g. dsh-layered-memory@0.5.4)
73
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory
74
+
75
+ # Option 2: with the dsh CLI installed (dsh is a pnpm forwarder; npm i -g pnpm first if missing)
76
+ dsh plugin --profile web add dsh-layered-memory
77
+
78
+ # Alternative sources: GitHub repo / local path (dev & debugging, link: points at the repo; npm run build + restart dsh to apply)
79
+ dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
80
+ dsh plugin --profile web add /path/to/dsh-layered-memory
81
+ ```
82
+
83
+ This package declares a `dsh.bundle` composition layer (`cordis.patch.yml`); after
84
+ installation the **plugin entry is mounted automatically** — no need to hand-edit
85
+ `$DSH_HOME/profiles/web/cordis.patch.yml`. Then restart DeepSeek Harness and verify:
86
+ the appearance of `conversations/ records/ scenes/` and `memory.db` under
87
+ `~/.dsh/memory/` means the plugin applied successfully; the "Memory" page in settings
88
+ and the mode pill in the input bar mean the client half is ready.
89
+
90
+ > ⚠️ **Security note**: installing a plugin = running third-party code with your
91
+ > privileges. This plugin reads session content, writes files in its data directory,
92
+ > and calls the LLM/embedding services you configured; if that concerns you, review
93
+ > the source first (`src/`).
94
+
95
+ **Uninstall**: `dsh plugin --profile web remove dsh-layered-memory` + restart. Data
96
+ stays in `~/.dsh/memory/`; delete the whole directory manually if you don't need it.
97
+
98
+ ### Development from Source
99
+
100
+ ```bash
101
+ git clone https://github.com/JunNanLYS/dsh-layered-memory
102
+ cd dsh-layered-memory
103
+ npm install && npm run build
104
+ dsh plugin --profile web add . # link: install; after code changes, npm run build + restart dsh
105
+ npm run smoke # smoke test (rebuild first: see command below)
106
+ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution nodenext --target es2022 --strict --skipLibCheck --esModuleInterop
107
+ ```
108
+
109
+ ## Configuration
110
+
111
+ Override configs go into the profile's own `cordis.patch.yml` as a **top-level bare
112
+ patch entry** (direct `id:`, not wrapped in `insert:` — an insert with the same id as
113
+ the bundle layer appends and causes `duplicate loader entry id` startup failure):
114
+
115
+ ```yaml
116
+ - id: dsh-memory
117
+ name: dsh-layered-memory
118
+ config: # keys replace whole lines (no deep merge); write out all keys you want to keep
119
+ family: auto # default mode for new sessions: auto | chat | work
120
+ llm: # distillation model route (falls back to the current default model if empty)
121
+ provider: ''
122
+ model: ''
123
+ ```
124
+
125
+ | Field | Default | Description |
126
+ | --- | --- | --- |
127
+ | `family` | `auto` | Default memory mode for new sessions: `auto` (both families) \| `chat` (personal) \| `work` (work); switchable per session via the input-bar control |
128
+ | `dataDir` | `$DSH_HOME/memory` | Data directory |
129
+ | `capture.enabled` | `true` | L0 capture |
130
+ | `capture.stripCodeBlocks` | `true` | Strip code blocks from assistant messages |
131
+ | `capture.maxMessageChars` | `4000` | Max characters per message |
132
+ | `extract.enabled` | `true` | L1 extraction |
133
+ | `extract.minMessages` | `1` | Run L1 extraction after N new messages accumulate |
134
+ | `extract.backgroundMessages` | `10` | Background messages attached to extraction |
135
+ | `extract.candidatePool` | `5` | Dedup candidate pool size |
136
+ | `l2.enabled` | `true` | L2 scene consolidation |
137
+ | `l2.minNewMemories` | `5` | New-memory threshold since last L2 consolidation |
138
+ | `l2.maxScenes` | `12` | Scene block count cap |
139
+ | `l2.sceneContextLimit` | `3` | Max similar-scene full texts attached to the L2 prompt |
140
+ | `l3.enabled` | `true` | L3 persona distillation |
141
+ | `l3.interval` | `20` | L3 distillation interval (new-memory count) |
142
+ | `recall.enabled` | `true` | Auto recall |
143
+ | `recall.maxResults` | `5` | L1 records injected per step |
144
+ | `recall.strategy` | `hybrid` | Retrieval strategy: `keyword` / `embedding` / `hybrid` |
145
+ | `recall.scoreThreshold` | `0.3` | Recall score threshold (below is not injected; applies to keyword/embedding only, not pre-fusion hybrid; tool path unfiltered) |
146
+ | `embedding.enabled` | `false` | Vector retrieval switch; off = pure FTS |
147
+ | `embedding.baseUrl` | empty | OpenAI-compatible /embeddings endpoint (e.g. `https://api.siliconflow.cn/v1`) |
148
+ | `embedding.apiKey` | empty | API key |
149
+ | `embedding.model` | empty | embedding model name |
150
+ | `embedding.dimensions` | `0` | Vector dimensions (required when enabled; must match model output) |
151
+ | `llm.provider/model` | empty | Distillation model override (defaults to current selection) |
152
+ | `llm.maxTokens` | `20000` | Output token cap per distillation call (unified across stages; a reasoning model's reasoning shares this budget — too low gets fully consumed by thinking, leaving 0 chars of text) |
153
+ | `llm.temperature` | `0.3` | Distillation temperature |
154
+ | `llm.maxInputChars` | `700000` | Input character budget per distillation call (over-budget L1 inputs are chunked automatically) |
155
+ | `tools` | `true` | Whether to register model-callable memory tools |
156
+
157
+ ## Storage Layout
158
+
159
+ Aligned with MemoryCore's official dual-write architecture: append-only JSONL files
160
+ (`conversations/`, `records/` sharded by day) are the source of truth for
161
+ backup/restore and are **never rewritten**; `memory.db` (node:sqlite + WAL + FTS5
162
+ BM25 + sqlite-vec cosine vectors) is the primary retrieval engine — updates/deletes
163
+ from dedup merges touch the retrieval DB only.
164
+
165
+ - **Three retrieval strategies** (`recall.strategy`): `keyword` (FTS5 BM25) /
166
+ `embedding` (vec0 cosine KNN) / `hybrid` (both lanes in parallel + RRF k=60
167
+ fusion, default); `conversation_search` (L0) uses the same fusion;
168
+ - **Vectors optional**: off by default (pure FTS). DSH's `ctx.llm` has no embeddings
169
+ endpoint; enabling requires any OpenAI-compatible `/embeddings` service (configure
170
+ `embedding.*`); config changes automatically drop the vector table and re-embed
171
+ everything in the background;
172
+ - **Degradation chain**: sqlite-vec load failure → pure FTS; embedding call failure
173
+ → degrade to FTS for that call with a one-time warning; retrieval DB init failure
174
+ → memory features disabled entirely but dsh itself starts normally;
175
+ - **Replacement seam**: `L1Store.search()` is the single retrieval entry point.
176
+
177
+ ```
178
+ memory/
179
+ ├── memory.db # SQLite retrieval DB (L0/L1 metadata + FTS5 + optional vectors; L1 has a family column)
180
+ ├── conversations/2026-01-01.jsonl # L0 raw conversation source of truth (one file per day, append-only; not family-split)
181
+ ├── records/2026-01-01.jsonl # L1 atomic memory source of truth (one file per day, append-only; family field included)
182
+ ├── scenes/chat/*.md # L2 scene blocks (chat family)
183
+ ├── scenes/work/*.md # L2 scene blocks (work family)
184
+ ├── persona-chat.md # L3 persona (chat family: user persona)
185
+ ├── persona-work.md # L3 persona (work family: Team Operating Doctrine)
186
+ ├── state.json # pipeline checkpoint (v2 family-split: families.chat / families.work)
187
+ ├── session-modes.json # session mode map (sessionId → mode, auto-pruned after >90 days)
188
+ └── memory.log # diagnostic log (info+, rotates to memory.log.1 beyond 2MB)
189
+ ```
190
+
191
+ ## Logging & Troubleshooting
192
+
193
+ The dsh host prints plugin logs to the console; the plugin mirrors info and above to
194
+ `memory.log` in its data directory. The typical log path of one conversation turn:
195
+ `L0 捕获 turn=N …条` → `L0 落盘 N 条` → `蒸馏管线开始(…待重试 M 条)` →
196
+ `LLM 调用 provider/model:输入 x → 输出 y 字符(z s)` → `L1 抽取完成(…新增 Y 条)` →
197
+ `蒸馏管线结束`; the next turn shows `召回命中 N 条 L1`. Empty LLM output carries full
198
+ diagnostics (finish reason / token counts / reasoning excerpt); JSON parse failures
199
+ include the first 400 characters of the raw model output; all failure warns carry the
200
+ first stack frame.
201
+
202
+ ## Differences from MemoryCore
203
+
204
+ - The full pipeline is embedded (no external Gateway); distillation reuses DSH's own LLM;
205
+ - L2/L3 changed from "LLM manipulates file tools" to "LLM outputs operation JSON / full documents, engineering side executes";
206
+ - Recall injection happens at `agent/pre-step` + agent-scoped `systemPrompt.context` (DSH-native events/services);
207
+ - Storage/retrieval is a single-machine slimmed version of the official sqlite backend (drops multi-tenant isolation columns, TCVDB cloud backend, audit tables; tokenization uses a bundled CJK bigram instead of jieba, keeping zero native dependencies — sqlite-vec is the only native extension, auto-degrading on load failure).
208
+
209
+ ## License
210
+
211
+ [MIT](LICENSE)
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ **简体中文** | [English](README.en.md)
2
+
1
3
  <p align="center">
2
4
  <img src="./assets/readme/hero.svg" width="100%"
3
5
  alt="dsh-layered-memory:对话自动分层蒸馏成记忆,模型每步前自动召回注入(L0 原始对话 → L1 原子记忆 → L2 场景块 → L3 核心画像)">
@@ -25,6 +27,16 @@ L0 捕获 → L1 原子记忆 → L2 场景整合 → L3 画像蒸馏,模型
25
27
  管线原则:所有蒸馏调用复用 DSH 自身的 `ctx.llm`;任何阶段失败只记日志、绝不阻塞
26
28
  Agent 循环;召回注入用标签包裹、捕获侧自动剥离,防止反馈循环。
27
29
 
30
+ **未蒸馏缓冲持久化**:抽取失败的待重试消息与攒触发阈值中途的消息都暂存在按档分桶的
31
+ 缓冲里(`pending.json`,每次蒸馏尝试后原子落盘)——重启不丢,启动 20 秒后自动补跑一次,
32
+ 失败则维持"等下一轮同档对话"的语义。
33
+
34
+ **重建**(设置页 → 记忆 → 概览 → 重建记忆):以 L0 原始对话为事实源重新推导全部派生层。
35
+ 旧 `records/`、`scenes/`、`persona-*.md` 整体归档(改名 `*.bak.<时间戳>`,不删除),检索库
36
+ L1 清空、checkpoint 重置,随后按会话分块、统一 auto 档重蒸馏 L1→L2→L3(收尾强制跑一轮
37
+ L2 残余 + L3 冷启动)。重建分块走低优先级队列——期间正常对话的蒸馏优先进行;带确认弹窗
38
+ (会话数/消息数/预计调用数)、进度条与取消(已重建部分保留)。
39
+
28
40
  ### 会话级记忆档位
29
41
 
30
42
  每个会话可独立选择记忆档位,**写入与召回同档**:
@@ -44,24 +56,26 @@ Agent 循环;召回注入用标签包裹、捕获侧自动剥离,防止反
44
56
 
45
57
  ### 记忆浏览器(设置页 → 记忆)
46
58
 
47
- 多 Tab 页面,两族混合视图:**概览**(各层计数 + 记忆模式开关面板,5 秒自动刷新)、
59
+ 多 Tab 页面,两族混合视图:**概览**(各层计数 + 记忆模式开关面板 + 蒸馏思考档位选择器,5 秒自动刷新)、
48
60
  **记忆**(L1 卡片列表,关键词/类型/情境筛选)、**场景**(L2 全文)、**画像**(L3 全文)、
49
- **日志**(`memory.log` 尾部 200 行)。开关走官方 settings 服务(命名空间 `dsh-memory`,
50
- 实时生效、重启保留);生效规则 = **静态 config(部署上限)AND 运行时开关**,
61
+ **日志**(`memory.log` 尾部 200 行)。开关与思考档位走官方 settings 服务(命名空间 `dsh-memory`,
62
+ 实时生效、重启保留);开关生效规则 = **静态 config(部署上限)AND 运行时开关**;
63
+ 思考档位为运行时覆盖(选择器选"跟随配置"则用部署配置 `llm.reasoningEffort` 作默认),
51
64
  数据通道为 loopback RPC(`dsh-memory/*`)。
52
65
 
53
66
  ## 快速开始
54
67
 
55
- 需要 Node ≥ 22.16 与已安装的 dsh CLI(dsh 是 pnpm 转发器,未装 pnpm 时先 `npm i -g pnpm`)。
68
+ 需要 Node ≥ 22.16。两种调用方式任选(`npx` 前缀可替换下面任何 `dsh` 命令):
56
69
 
57
70
  ```bash
58
- # npm 安装(推荐;可 pin 版本,如 @0.5.3
71
+ # 方式一:npx 直接跑官方 CLI(无需预装 dsh;可 pin 版本,如 dsh-layered-memory@0.5.4
72
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory
73
+
74
+ # 方式二:已装 dsh CLI(dsh 是 pnpm 转发器,未装 pnpm 时先 npm i -g pnpm)
59
75
  dsh plugin --profile web add dsh-layered-memory
60
76
 
61
- # 或从 GitHub 仓库安装
77
+ # 包源备选:GitHub 仓库 / 本地路径(开发调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
62
78
  dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
63
-
64
- # 或从本地路径安装(开发/调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
65
79
  dsh plugin --profile web add /path/to/dsh-layered-memory
66
80
  ```
67
81
 
@@ -130,7 +144,8 @@ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution no
130
144
  | `embedding.model` | 空 | embedding 模型名 |
131
145
  | `embedding.dimensions` | `0` | 向量维度(启用时必填,须与模型输出一致) |
132
146
  | `llm.provider/model` | 空 | 蒸馏模型覆盖(默认用当前默认选择) |
133
- | `llm.maxTokens` | `20000` | 单次蒸馏输出 token 上限(全阶段统一;推理模型的 reasoning 与正文共享该预算,过低会被思考吃光导致正文 0 字符) |
147
+ | `llm.maxTokens` | `256000` | 单次蒸馏输出 token 上限(全阶段统一;推理模型的 reasoning 与正文共享该预算,过低会被思考吃光导致正文 0 字符) |
148
+ | `llm.reasoningEffort` | `off` | 蒸馏思考档位(部署默认):`off` / `high` / `max`,空串不传(跟随模型默认)。蒸馏是结构化抽取任务,默认关思考——推理模型(如 v4-flash)默认 high 档的思考可把任意输出预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时需设为空串。运行时可在设置页 → 记忆 → 概览临时切换(选"跟随配置"即回退本值) |
134
149
  | `llm.temperature` | `0.3` | 蒸馏温度 |
135
150
  | `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取) |
136
151
  | `tools` | `true` | 是否注册模型可调用的记忆工具 |
@@ -160,7 +175,9 @@ memory/
160
175
  ├── persona-chat.md # L3 画像(chat 族:用户画像)
161
176
  ├── persona-work.md # L3 画像(work 族:Team Operating Doctrine)
162
177
  ├── state.json # 管线 checkpoint(v2 分族:families.chat / families.work)
178
+ ├── pending.json # 未蒸馏缓冲(按档分桶;重启恢复 + 启动自动补跑)
163
179
  ├── session-modes.json # 会话档位映射(sessionId → 档位,>90 天自动清理)
180
+ ├── records.bak.<ts>/ # 重建时归档的旧产物(scenes/persona 同款 *.bak.<ts>)
164
181
  └── memory.log # 诊断日志(info+,超 2MB 轮转为 memory.log.1)
165
182
  ```
166
183