dsh-layered-memory 0.5.3 → 0.6.0

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 核心画像)">
@@ -44,24 +46,26 @@ Agent 循环;召回注入用标签包裹、捕获侧自动剥离,防止反
44
46
 
45
47
  ### 记忆浏览器(设置页 → 记忆)
46
48
 
47
- 多 Tab 页面,两族混合视图:**概览**(各层计数 + 记忆模式开关面板,5 秒自动刷新)、
49
+ 多 Tab 页面,两族混合视图:**概览**(各层计数 + 记忆模式开关面板 + 蒸馏思考档位选择器,5 秒自动刷新)、
48
50
  **记忆**(L1 卡片列表,关键词/类型/情境筛选)、**场景**(L2 全文)、**画像**(L3 全文)、
49
- **日志**(`memory.log` 尾部 200 行)。开关走官方 settings 服务(命名空间 `dsh-memory`,
50
- 实时生效、重启保留);生效规则 = **静态 config(部署上限)AND 运行时开关**,
51
+ **日志**(`memory.log` 尾部 200 行)。开关与思考档位走官方 settings 服务(命名空间 `dsh-memory`,
52
+ 实时生效、重启保留);开关生效规则 = **静态 config(部署上限)AND 运行时开关**;
53
+ 思考档位为运行时覆盖(选择器选"跟随配置"则用部署配置 `llm.reasoningEffort` 作默认),
51
54
  数据通道为 loopback RPC(`dsh-memory/*`)。
52
55
 
53
56
  ## 快速开始
54
57
 
55
- 需要 Node ≥ 22.16 与已安装的 dsh CLI(dsh 是 pnpm 转发器,未装 pnpm 时先 `npm i -g pnpm`)。
58
+ 需要 Node ≥ 22.16。两种调用方式任选(`npx` 前缀可替换下面任何 `dsh` 命令):
56
59
 
57
60
  ```bash
58
- # npm 安装(推荐;可 pin 版本,如 @0.5.3
61
+ # 方式一:npx 直接跑官方 CLI(无需预装 dsh;可 pin 版本,如 dsh-layered-memory@0.5.4
62
+ npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory
63
+
64
+ # 方式二:已装 dsh CLI(dsh 是 pnpm 转发器,未装 pnpm 时先 npm i -g pnpm)
59
65
  dsh plugin --profile web add dsh-layered-memory
60
66
 
61
- # 或从 GitHub 仓库安装
67
+ # 包源备选:GitHub 仓库 / 本地路径(开发调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
62
68
  dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
63
-
64
- # 或从本地路径安装(开发/调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
65
69
  dsh plugin --profile web add /path/to/dsh-layered-memory
66
70
  ```
67
71
 
@@ -131,6 +135,7 @@ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution no
131
135
  | `embedding.dimensions` | `0` | 向量维度(启用时必填,须与模型输出一致) |
132
136
  | `llm.provider/model` | 空 | 蒸馏模型覆盖(默认用当前默认选择) |
133
137
  | `llm.maxTokens` | `20000` | 单次蒸馏输出 token 上限(全阶段统一;推理模型的 reasoning 与正文共享该预算,过低会被思考吃光导致正文 0 字符) |
138
+ | `llm.reasoningEffort` | `off` | 蒸馏思考档位(部署默认):`off` / `high` / `max`,空串不传(跟随模型默认)。蒸馏是结构化抽取任务,默认关思考——推理模型(如 v4-flash)默认 high 档的思考可把任意输出预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时需设为空串。运行时可在设置页 → 记忆 → 概览临时切换(选"跟随配置"即回退本值) |
134
139
  | `llm.temperature` | `0.3` | 蒸馏温度 |
135
140
  | `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取) |
136
141
  | `tools` | `true` | 是否注册模型可调用的记忆工具 |
package/dist/client.js CHANGED
@@ -160,6 +160,31 @@ window.__ModuleLoader__.load({
160
160
  padding: "4px 12px",
161
161
  marginBottom: 14,
162
162
  },
163
+ seg: {
164
+ display: "inline-flex",
165
+ border: "1px solid var(--dsw-alias-border-secondary, #e5e5e5)",
166
+ borderRadius: 8,
167
+ overflow: "hidden",
168
+ flexShrink: 0,
169
+ },
170
+ segBtn: {
171
+ padding: "4px 12px",
172
+ fontSize: 12,
173
+ lineHeight: "16px",
174
+ cursor: "pointer",
175
+ background: "transparent",
176
+ color: "inherit",
177
+ border: "none",
178
+ borderRight: "1px solid var(--dsw-alias-border-secondary, #e5e5e5)",
179
+ },
180
+ segBtnOn: {
181
+ background: "var(--dsw-alias-interactive-bg-primary, #1a7f37)",
182
+ color: "#fff",
183
+ fontWeight: 600,
184
+ },
185
+ segBtnOff: {
186
+ background: "var(--dsw-alias-bg-secondary, #f6f8fa)",
187
+ },
163
188
  flexRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
164
189
  grow: { flex: 1 },
165
190
  sceneHead: { display: "flex", alignItems: "baseline", gap: 10, marginBottom: 6, flexWrap: "wrap" },
@@ -228,6 +253,36 @@ window.__ModuleLoader__.load({
228
253
  );
229
254
  }
230
255
 
256
+ // ── Segmented 组件:分段选择器(记忆设置页的蒸馏思考档位等单选项) ──
257
+ function Segmented(props) {
258
+ var value = props.value;
259
+ var disabled = !!props.disabled;
260
+ return react.createElement(
261
+ "div",
262
+ { style: Object.assign({}, S.seg, disabled ? S.switchDisabled : null) },
263
+ props.options.map(function (opt, i) {
264
+ var on = opt.key === value;
265
+ return react.createElement(
266
+ "span",
267
+ {
268
+ key: opt.key,
269
+ style: Object.assign(
270
+ {},
271
+ S.segBtn,
272
+ on ? S.segBtnOn : S.segBtnOff,
273
+ i === props.options.length - 1 ? { borderRight: "none" } : null,
274
+ disabled ? { cursor: "not-allowed" } : null,
275
+ ),
276
+ onClick: function () {
277
+ if (!disabled && !on && props.onChange) props.onChange(opt.key);
278
+ },
279
+ },
280
+ opt.label,
281
+ );
282
+ }),
283
+ );
284
+ }
285
+
231
286
  // ── 会话记忆档位控件(输入栏 pill + macOS 风格滑动选择器) ──
232
287
  // 档位顺序即滑轨顺序:关闭 → chat → work → 自动(默认档"自动"居右)
233
288
  var MODES = [
@@ -257,40 +312,54 @@ window.__ModuleLoader__.load({
257
312
  return 3;
258
313
  }
259
314
 
260
- /** 滑动选择器浮层(参考 macOS 滑动器:拖拽圆头 + 松手吸附最近档;当前档由下方标签高亮指示)。 */
315
+ /** 滑动选择器浮层(参考 macOS 滑动器:拖拽圆头 1:1 连续跟手,松手按动量投影吸附最近档)。 */
261
316
  function ModeSlider(props) {
317
+ ensureFlowStyle(); // 玻璃材质 class 与流光共用同一张注入样式表
262
318
  var trackRef = react.useRef(null);
319
+ // 拖拽状态:{ x: 圆头连续位置 px, lastX: 上次指针 clientX, t: 时间戳, v: 速度 px/ms(EMA 平滑) }
263
320
  var dragState = react.useState(null);
264
- var dragIdx = dragState[0];
265
- var setDragIdx = dragState[1];
266
-
267
- var activeIdx = dragIdx === null ? modeIndex(props.mode) : dragIdx;
321
+ var drag = dragState[0];
322
+ var setDrag = dragState[1];
268
323
 
269
- var idxFromClientX = function (clientX) {
324
+ var clampX = function (x) {
325
+ if (x < 0) return 0;
326
+ if (x > INNER_W) return INNER_W;
327
+ return x;
328
+ };
329
+ var xFromClientX = function (clientX) {
270
330
  var rect = trackRef.current.getBoundingClientRect();
271
- var x = clientX - rect.left - THUMB / 2;
272
- if (x < 0) x = 0;
273
- if (x > INNER_W) x = INNER_W;
274
- return Math.round((x / INNER_W) * (MODES.length - 1));
331
+ return clampX(clientX - rect.left - THUMB / 2);
275
332
  };
276
333
 
277
334
  var onPointerDown = function (e) {
278
335
  e.preventDefault();
279
336
  e.currentTarget.setPointerCapture(e.pointerId);
280
- setDragIdx(idxFromClientX(e.clientX));
337
+ setDrag({ x: xFromClientX(e.clientX), lastX: e.clientX, t: e.timeStamp, v: 0 });
281
338
  };
282
339
  var onPointerMove = function (e) {
283
- if (dragIdx === null) return;
284
- setDragIdx(idxFromClientX(e.clientX));
340
+ if (drag === null) return;
341
+ var dt = e.timeStamp - drag.t;
342
+ var instV = dt > 0 ? (e.clientX - drag.lastX) / dt : drag.v;
343
+ setDrag({
344
+ x: xFromClientX(e.clientX),
345
+ lastX: e.clientX,
346
+ t: e.timeStamp,
347
+ v: drag.v * 0.7 + instV * 0.3, // EMA:瞬时抖动不放大,松手投影用
348
+ });
285
349
  };
286
350
  var onPointerUp = function (e) {
287
- if (dragIdx === null) return;
288
- var idx = idxFromClientX(e.clientX);
289
- setDragIdx(null);
351
+ if (drag === null) return;
352
+ // 动量投影(Designing Fluid Interfaces):按松手速度前瞻落点就近吸附;
353
+ // 投影量 clamp 到半档(±30px)——甩动最多把边界推到相邻档,绝不会跳两档
354
+ var projected = xFromClientX(e.clientX) + Math.max(-30, Math.min(30, drag.v * 120));
355
+ var idx = Math.round((clampX(projected) / INNER_W) * (MODES.length - 1));
356
+ setDrag(null);
290
357
  props.onCommit(MODES[idx].key);
291
358
  };
292
359
 
293
- var thumbLeft = (activeIdx / (MODES.length - 1)) * INNER_W;
360
+ // 拖拽中圆头 1:1 跟指针(连续位置,不吸附);静止时停在档位中心
361
+ var thumbLeft = drag !== null ? drag.x : (modeIndex(props.mode) / (MODES.length - 1)) * INNER_W;
362
+ var activeIdx = Math.min(MODES.length - 1, Math.max(0, Math.round((thumbLeft / INNER_W) * (MODES.length - 1))));
294
363
  var info = MODES[activeIdx];
295
364
 
296
365
  var stops = [];
@@ -310,7 +379,8 @@ window.__ModuleLoader__.load({
310
379
  width: 6,
311
380
  height: 6,
312
381
  borderRadius: "50%",
313
- background: active ? MODES[i].color : "#c6cbd1",
382
+ background: active ? MODES[i].color : "rgba(128,140,150,0.55)",
383
+ zIndex: 2,
314
384
  },
315
385
  },
316
386
  ),
@@ -331,9 +401,10 @@ window.__ModuleLoader__.load({
331
401
  cursor: "pointer",
332
402
  whiteSpace: "nowrap",
333
403
  fontWeight: active ? 600 : 400,
334
- color: active ? MODES[i].color : "var(--dsw-alias-label-tertiary, #888)",
404
+ // 未激活标签走主题 caption 令牌(浅色 #400 灰蓝 / 暗色 #600),玻璃面上保持可读
405
+ color: active ? MODES[i].color : "var(--dsw-alias-label-caption, #888)",
335
406
  },
336
- onClick: function () { if (dragIdx === null) props.onCommit(MODES[i].key); },
407
+ onClick: function () { if (drag === null) props.onCommit(MODES[i].key); },
337
408
  },
338
409
  MODES[i].label,
339
410
  ),
@@ -344,6 +415,8 @@ window.__ModuleLoader__.load({
344
415
  return react.createElement(
345
416
  "div",
346
417
  {
418
+ // 外壳只负责定位(带 transform 居中);玻璃材质拆到内层独立元素——
419
+ // Chromium 中 transform 元素上的 backdrop-filter 采样异常,玻璃会失效
347
420
  style: {
348
421
  position: "absolute",
349
422
  bottom: "calc(100% + 8px)",
@@ -351,67 +424,134 @@ window.__ModuleLoader__.load({
351
424
  left: "50%",
352
425
  transform: "translateX(-50%)",
353
426
  zIndex: 1000,
354
- background: "var(--dsw-alias-bg-primary, #fff)",
355
- border: "1px solid var(--dsw-alias-border-secondary, #e0e0e0)",
356
- borderRadius: 10,
357
- boxShadow: "0 8px 24px rgba(0,0,0,0.16)",
358
- padding: "12px 16px 30px",
359
427
  },
360
428
  },
429
+ react.createElement("div", {
430
+ className: "dsh-mem-glass",
431
+ style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, pointerEvents: "none" },
432
+ }),
361
433
  react.createElement(
362
434
  "div",
363
- {
364
- ref: trackRef,
365
- style: {
366
- position: "relative",
367
- width: TRACK_W + THUMB,
368
- height: 16,
369
- touchAction: "none",
370
- cursor: dragIdx === null ? "pointer" : "grabbing",
435
+ { style: { position: "relative", padding: "12px 16px 30px" } },
436
+ react.createElement(
437
+ "div",
438
+ {
439
+ ref: trackRef,
440
+ style: {
441
+ position: "relative",
442
+ // 容器宽 = thumb 活动范围(0..INNER_W + THUMB),点击映射与视觉两端严格对齐
443
+ width: TRACK_W,
444
+ height: 16,
445
+ touchAction: "none",
446
+ cursor: drag === null ? "pointer" : "grabbing",
371
447
  },
372
448
  onPointerDown: onPointerDown,
373
449
  onPointerMove: onPointerMove,
374
450
  onPointerUp: onPointerUp,
375
451
  onPointerCancel: onPointerUp,
376
452
  },
377
- // 圆球先画、线条后画(叠在球面上)——线条从圆球中心穿过;
378
- // 线两端延伸到两端球的边缘(对称穿过首尾停点的中心);
379
- // 当前档由下方标签高亮指示(上方不放文字)
453
+ // 线条(底层):只连首尾停点的中心,两端不再露出停点外;
454
+ // thumb(当前档定位球)压最上层,不再被线条与停点穿过。
455
+ // 中性半透明灰在玻璃材质上深浅主题都可辨
380
456
  react.createElement("div", {
381
457
  style: {
382
458
  position: "absolute",
383
- left: thumbLeft,
384
- top: 1,
385
- width: THUMB,
386
- height: THUMB,
387
- borderRadius: "50%",
388
- background: "#fff",
389
- border: "1px solid " + info.color,
390
- boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
459
+ left: THUMB / 2,
460
+ width: INNER_W,
461
+ top: 7,
462
+ height: 4,
463
+ borderRadius: 999,
464
+ background: "rgba(128,140,150,0.32)",
391
465
  pointerEvents: "none",
392
- transition: dragIdx === null ? "left 120ms ease" : "none",
466
+ zIndex: 1,
393
467
  },
394
468
  }),
469
+ stops,
395
470
  react.createElement("div", {
396
471
  style: {
397
472
  position: "absolute",
398
- left: 0,
399
- width: TRACK_W,
400
- top: 7,
401
- height: 4,
402
- borderRadius: 999,
403
- background: "var(--dsw-alias-border-secondary, #e1e4e8)",
473
+ left: thumbLeft,
474
+ top: 1,
475
+ width: THUMB,
476
+ height: THUMB,
477
+ borderRadius: "50%",
478
+ // 微透白 + 上缘高光:与浮层玻璃材质同语言
479
+ background: "rgba(255,255,255,0.94)",
480
+ border: "1px solid " + info.color,
481
+ boxShadow: "0 1px 4px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.9)",
404
482
  pointerEvents: "none",
483
+ transition: drag === null ? "left 120ms ease" : "none",
484
+ zIndex: 3,
405
485
  },
406
486
  }),
407
- stops,
487
+ ),
488
+ props.error
489
+ ? react.createElement("div", { style: { fontSize: 11, color: "#cf222e", marginTop: 20, whiteSpace: "nowrap" } }, props.error)
490
+ : null,
408
491
  ),
409
- props.error
410
- ? react.createElement("div", { style: { fontSize: 11, color: "#cf222e", marginTop: 20, whiteSpace: "nowrap" } }, props.error)
411
- : null,
412
492
  );
413
493
  }
414
494
 
495
+ // ── auto 档边缘流光 + 浮层玻璃材质:inline style 放不了 @keyframes/@property/媒体查询,
496
+ // 惰性注入一次性样式表(id 防重复)。dsw 主题变量参考其前端令牌:暗色走 body[data-ds-dark-theme]。 ──
497
+ var FLOW_STYLE_ID = "dsh-mem-flow-style";
498
+ function ensureFlowStyle() {
499
+ if (document.getElementById(FLOW_STYLE_ID)) return;
500
+ var el = document.createElement("style");
501
+ el.id = FLOW_STYLE_ID;
502
+ el.textContent = [
503
+ // conic 角度动画需要 @property 注册才能插值;不支持的浏览器优雅降级为静态渐变边框
504
+ "@property --dsh-mem-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }",
505
+ "@keyframes dshMemFlow { to { --dsh-mem-angle: 360deg; } }",
506
+ // 边缘流光(双层背景):border 区画旋转 conic 冷蓝光带;内部必须是【不透明】底色盖住
507
+ // 光带(半透明内层会让 conic 透进按钮内部,文字被光斑干扰——实测事故)。
508
+ // 不透明底 = 主题底混 12% 冷蓝(color-mix 出来 alpha=1),静态、随主题
509
+ ".dsh-mem-flow {",
510
+ " border: 1px solid transparent;",
511
+ " background:",
512
+ " linear-gradient(",
513
+ " color-mix(in srgb, var(--dsw-alias-bg-layer-2, #ffffff) 88%, #3b82f6),",
514
+ " color-mix(in srgb, var(--dsw-alias-bg-layer-2, #ffffff) 88%, #3b82f6)",
515
+ " ) padding-box,",
516
+ " conic-gradient(from var(--dsh-mem-angle),",
517
+ " rgba(8,145,178,0.9), rgba(59,130,246,0.9), rgba(125,211,252,1),",
518
+ " rgba(99,102,241,0.9), rgba(8,145,178,0.9)) border-box;",
519
+ " animation: dshMemFlow 3s linear infinite;",
520
+ " color: #0284c7;",
521
+ "}",
522
+ "body[data-ds-dark-theme] .dsh-mem-flow { color: #7dd3fc; }",
523
+ "@media (prefers-reduced-motion: reduce) { .dsh-mem-flow { animation: none; } }",
524
+ // 浮层玻璃材质(Apple 菜单配方):材质层必须是【无 transform 的独立元素】——
525
+ // Chromium 中 transform 元素上的 backdrop-filter 采样异常(玻璃失效,实测)。
526
+ // 低不透明度基底让底下内容透出 + 顶部光泽渐变 + blur/saturate + 亮缘 + 分主题阴影
527
+ ".dsh-mem-glass {",
528
+ " border-radius: 12px;",
529
+ " background:",
530
+ " linear-gradient(rgba(255,255,255,0.34), rgba(255,255,255,0.05)),",
531
+ " rgba(248,249,251,0.55);",
532
+ " -webkit-backdrop-filter: blur(32px) saturate(180%);",
533
+ " backdrop-filter: blur(32px) saturate(180%);",
534
+ " border: 1px solid rgba(255,255,255,0.55);",
535
+ " box-shadow:",
536
+ " 0 12px 40px rgba(0,0,0,0.16), 0 2px 8px rgba(0,0,0,0.07),",
537
+ " inset 0 1px 0 rgba(255,255,255,0.55), inset 0 -1px 0 rgba(0,0,0,0.03);",
538
+ "}",
539
+ "body[data-ds-dark-theme] .dsh-mem-glass {",
540
+ " background:",
541
+ " linear-gradient(rgba(255,255,255,0.09), rgba(255,255,255,0.015)),",
542
+ " rgba(30,32,40,0.55);",
543
+ " border-color: rgba(255,255,255,0.14);",
544
+ " box-shadow:",
545
+ " 0 16px 48px rgba(0,0,0,0.55), 0 3px 12px rgba(0,0,0,0.35),",
546
+ " inset 0 1px 0 rgba(255,255,255,0.12);",
547
+ "}",
548
+ "@media (prefers-reduced-transparency: reduce) {",
549
+ " .dsh-mem-glass { background: var(--dsw-alias-bg-layer-2, #ffffff); backdrop-filter: none; -webkit-backdrop-filter: none; }",
550
+ "}",
551
+ ].join("\n");
552
+ document.head.appendChild(el);
553
+ }
554
+
415
555
  /** 输入栏 pill:点击展开滑动选择器;props 来自 conversation.input.left 的 zone 注入。 */
416
556
  function MemoryModePill(props) {
417
557
  var rpc = props.rpc;
@@ -474,6 +614,31 @@ window.__ModuleLoader__.load({
474
614
  if (!sessionId || !rpc) return null;
475
615
  var info = modeInfo(mode);
476
616
  var loaded = mode !== null;
617
+ var isAuto = loaded && mode === "auto";
618
+
619
+ if (isAuto) ensureFlowStyle();
620
+
621
+ // auto 档的边框/背景/文字色均由 .dsh-mem-flow 提供(双层背景画流光边 + 分主题文字),
622
+ // inline 不能设置这些——inline 优先级会盖掉 class 里的流光
623
+ var pillStyle = {
624
+ display: "inline-flex",
625
+ alignItems: "center",
626
+ gap: 4,
627
+ height: 24,
628
+ padding: "0 10px",
629
+ borderRadius: 999,
630
+ fontSize: 12,
631
+ fontWeight: 500,
632
+ lineHeight: "20px",
633
+ cursor: "pointer",
634
+ };
635
+ if (isAuto) {
636
+ pillStyle.boxShadow = "0 0 12px rgba(56,189,248,0.30)";
637
+ } else {
638
+ pillStyle.border = "1px solid " + (loaded ? info.color + "66" : "var(--dsw-alias-border-secondary, #ccc)");
639
+ pillStyle.background = loaded ? info.color + "14" : "var(--dsw-alias-bg-secondary, #f6f8fa)";
640
+ pillStyle.color = loaded ? info.color : "var(--dsh-alias-label-tertiary, #888)";
641
+ }
477
642
 
478
643
  return react.createElement(
479
644
  "div",
@@ -484,21 +649,8 @@ window.__ModuleLoader__.load({
484
649
  type: "button",
485
650
  title: "本会话记忆档位(点击切换)",
486
651
  onClick: function () { setOpen(!open); },
487
- style: {
488
- display: "inline-flex",
489
- alignItems: "center",
490
- gap: 4,
491
- height: 24,
492
- padding: "0 10px",
493
- borderRadius: 999,
494
- fontSize: 12,
495
- fontWeight: 500,
496
- lineHeight: "20px",
497
- cursor: "pointer",
498
- border: "1px solid " + (loaded ? info.color + "66" : "var(--dsw-alias-border-secondary, #ccc)"),
499
- background: loaded ? info.color + "14" : "var(--dsw-alias-bg-secondary, #f6f8fa)",
500
- color: loaded ? info.color : "var(--dsh-alias-label-tertiary, #888)",
501
- },
652
+ className: isAuto ? "dsh-mem-flow" : null,
653
+ style: pillStyle,
502
654
  },
503
655
  "记忆·",
504
656
  react.createElement("span", null, loaded ? info.label : "…"),
@@ -630,6 +782,31 @@ window.__ModuleLoader__.load({
630
782
  disabled: !master,
631
783
  onChange: function (v) { toggle("recall", v); },
632
784
  }),
785
+ settingsData.effort
786
+ ? react.createElement(
787
+ "div",
788
+ { style: S.switchRow },
789
+ react.createElement(Segmented, {
790
+ value: settingsData.effort.current,
791
+ options: [
792
+ { key: "", label: "跟随配置" },
793
+ { key: "off", label: "off" },
794
+ { key: "high", label: "high" },
795
+ { key: "max", label: "max" },
796
+ ],
797
+ disabled: !master,
798
+ onChange: function (v) { toggle("reasoningEffort", v); },
799
+ }),
800
+ react.createElement("div", null,
801
+ react.createElement("div", { style: S.switchLabel }, "蒸馏思考"),
802
+ react.createElement(
803
+ "div",
804
+ { style: S.switchDesc },
805
+ "当前生效 " + (settingsData.effort.effective || "(不传,跟随模型默认)") +
806
+ (settingsData.effort.current ? "" : "(来自部署配置 llm.reasoningEffort)"),
807
+ )),
808
+ )
809
+ : null,
633
810
  ceilingNote
634
811
  ? react.createElement("p", { style: S.hint }, ceilingNote)
635
812
  : null,
package/dist/config.d.ts CHANGED
@@ -72,6 +72,8 @@ export interface MemoryConfig {
72
72
  model: string;
73
73
  /** 单次蒸馏调用的输出 token 上限(推理模型的 reasoning 与正文共享该预算)。 */
74
74
  maxTokens: number;
75
+ /** 蒸馏调用的思考档位;空串不传(跟随模型默认)。 */
76
+ reasoningEffort: string;
75
77
  temperature: number;
76
78
  /** 单次蒸馏调用的用户 prompt 字符预算(≈token 数,按中文 1 字≈1 token 保守估算)。 */
77
79
  maxInputChars: number;
@@ -158,6 +160,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
158
160
  provider: Schema<string, string>;
159
161
  model: Schema<string, string>;
160
162
  maxTokens: Schema<number, number>;
163
+ reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
161
164
  temperature: Schema<number, number>;
162
165
  maxInputChars: Schema<number, number>;
163
166
  timeoutMs: Schema<number, number>;
@@ -165,6 +168,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
165
168
  provider: Schema<string, string>;
166
169
  model: Schema<string, string>;
167
170
  maxTokens: Schema<number, number>;
171
+ reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
168
172
  temperature: Schema<number, number>;
169
173
  maxInputChars: Schema<number, number>;
170
174
  timeoutMs: Schema<number, number>;
@@ -247,6 +251,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
247
251
  provider: Schema<string, string>;
248
252
  model: Schema<string, string>;
249
253
  maxTokens: Schema<number, number>;
254
+ reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
250
255
  temperature: Schema<number, number>;
251
256
  maxInputChars: Schema<number, number>;
252
257
  timeoutMs: Schema<number, number>;
@@ -254,6 +259,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
254
259
  provider: Schema<string, string>;
255
260
  model: Schema<string, string>;
256
261
  maxTokens: Schema<number, number>;
262
+ reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
257
263
  temperature: Schema<number, number>;
258
264
  maxInputChars: Schema<number, number>;
259
265
  timeoutMs: Schema<number, number>;
package/dist/config.js CHANGED
@@ -51,6 +51,9 @@ export const memorySchema = Schema.object({
51
51
  model: Schema.string().default(''),
52
52
  // 推理模型(如 v4-flash)的 reasoning 计入输出预算:预算不足会被思考吃光导致正文 0 字符
53
53
  maxTokens: Schema.number().default(20_000),
54
+ // 蒸馏是结构化抽取任务,默认关思考(off):v4-flash 默认 high 档的思考可把任意 maxTokens
55
+ // 预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时会报 UNSUPPORTED_REASONING_EFFORT,设空串跳过
56
+ reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default('off'),
54
57
  temperature: Schema.number().default(0.3),
55
58
  // 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
56
59
  maxInputChars: Schema.number().default(700_000),
package/dist/index.d.ts CHANGED
@@ -85,6 +85,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
85
85
  provider: import("@deepseek-ai/schemastery").default<string, string>;
86
86
  model: import("@deepseek-ai/schemastery").default<string, string>;
87
87
  maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
88
+ reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
88
89
  temperature: import("@deepseek-ai/schemastery").default<number, number>;
89
90
  maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
90
91
  timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
@@ -92,6 +93,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
92
93
  provider: import("@deepseek-ai/schemastery").default<string, string>;
93
94
  model: import("@deepseek-ai/schemastery").default<string, string>;
94
95
  maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
96
+ reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
95
97
  temperature: import("@deepseek-ai/schemastery").default<number, number>;
96
98
  maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
97
99
  timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
@@ -174,6 +176,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
174
176
  provider: import("@deepseek-ai/schemastery").default<string, string>;
175
177
  model: import("@deepseek-ai/schemastery").default<string, string>;
176
178
  maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
179
+ reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
177
180
  temperature: import("@deepseek-ai/schemastery").default<number, number>;
178
181
  maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
179
182
  timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
@@ -181,6 +184,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
181
184
  provider: import("@deepseek-ai/schemastery").default<string, string>;
182
185
  model: import("@deepseek-ai/schemastery").default<string, string>;
183
186
  maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
187
+ reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
184
188
  temperature: import("@deepseek-ai/schemastery").default<number, number>;
185
189
  maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
186
190
  timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
package/dist/llm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createUserMessage } from '@deepseek-ai/dsh-llm';
1
+ import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
2
2
  import { errDetail } from './util/filelog.js';
3
3
  /** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
4
4
  export async function resolveModelRoute(ctx, cfg) {
@@ -35,6 +35,10 @@ export async function callLLM(ctx, cfg, opts) {
35
35
  messages: [createUserMessage({ content: [{ type: 'text', text: user }], source: { kind: 'user' } })],
36
36
  temperature: opts.temperature ?? cfg.llm.temperature,
37
37
  maxTokens: opts.maxTokens ?? cfg.llm.maxTokens,
38
+ // 默认 off:蒸馏是结构化抽取,high 思考可吃光全部输出预算致正文 0 字符;空串不传(非推理模型)
39
+ ...(cfg.llm.reasoningEffort
40
+ ? { reasoningEffort: ReasoningEffortId(cfg.llm.reasoningEffort) }
41
+ : {}),
38
42
  signal,
39
43
  });
40
44
  const startedAt = Date.now();
@@ -51,20 +51,26 @@ export class MemoryRunner {
51
51
  const turnStart = Date.now();
52
52
  this.logger.info(`[memory] 蒸馏管线开始(session=${sessionId},mode=${mode},本轮 ${messages.length} 条消息,待重试 ${this.pendingCount} 条)`);
53
53
  // ── L0:原始对话已由 capture 在 turn/end 即时落盘(不排蒸馏队列,防慢 LLM 阻塞/退出丢消息) ──
54
+ // ── 运行时调参视图:UI 选择器可临时覆盖蒸馏思考档位(空串回退静态 config 默认)。
55
+ // 浅拷贝只覆盖 llm 一层,其余键与 this.cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。 ──
56
+ const liveNow = this.live.get();
57
+ const cfg = liveNow.reasoningEffort
58
+ ? { ...this.cfg, llm: { ...this.cfg.llm, reasoningEffort: liveNow.reasoningEffort } }
59
+ : this.cfg;
54
60
  // ── L1:抽取 + 去重(按档分桶,失败按桶保留待重试) ──
55
61
  let newRecords = [];
56
- const distillOn = this.live.get().enabled && this.live.get().distill;
57
- if (this.cfg.extract.enabled && distillOn) {
62
+ const distillOn = liveNow.enabled && liveNow.distill;
63
+ if (cfg.extract.enabled && distillOn) {
58
64
  const bucket = this.pending[mode];
59
65
  bucket.push(...messages);
60
66
  if (bucket.length > 200)
61
67
  bucket.splice(0, bucket.length - 200);
62
68
  try {
63
69
  const t = Date.now();
64
- const result = await runExtraction(this.ctx, this.cfg, this.stores.l1, this.states, bucket, this.background, this.logger, mode);
70
+ const result = await runExtraction(this.ctx, cfg, this.stores.l1, this.states, bucket, this.background, this.logger, mode);
65
71
  if (!result.skipped)
66
72
  this.pending[mode] = [];
67
- this.background = [...this.background.slice(-this.cfg.extract.backgroundMessages), ...messages];
73
+ this.background = [...this.background.slice(-cfg.extract.backgroundMessages), ...messages];
68
74
  newRecords = result.newRecords;
69
75
  this.logger.info(`[memory] L1 阶段完成(${Date.now() - t}ms)`);
70
76
  }
@@ -84,16 +90,16 @@ export class MemoryRunner {
84
90
  }
85
91
  }
86
92
  // ── L2/L3:按记录族各自判定与执行 ──
87
- if (this.cfg.l2.enabled && distillOn) {
93
+ if (cfg.l2.enabled && distillOn) {
88
94
  for (const family of ['chat', 'work']) {
89
95
  const familyRecords = newRecords.filter((r) => (r.family ?? 'chat') === family);
90
96
  if (familyRecords.length === 0)
91
97
  continue;
92
98
  const fstate = this.states[family];
93
- if (fstate.newMemoriesSinceL2 >= this.cfg.l2.minNewMemories) {
99
+ if (fstate.newMemoriesSinceL2 >= cfg.l2.minNewMemories) {
94
100
  try {
95
101
  const t = Date.now();
96
- const result = await runSceneConsolidation(this.ctx, this.cfg, this.stores.scenes[family], familyRecords, this.logger, family);
102
+ const result = await runSceneConsolidation(this.ctx, cfg, this.stores.scenes[family], familyRecords, this.logger, family);
97
103
  fstate.lastL2At = Date.now();
98
104
  fstate.newMemoriesSinceL2 = 0;
99
105
  if (result.personaRequestedReason)
@@ -105,14 +111,14 @@ export class MemoryRunner {
105
111
  }
106
112
  }
107
113
  else {
108
- this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${this.cfg.l2.minNewMemories})`);
114
+ this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${cfg.l2.minNewMemories})`);
109
115
  }
110
116
  }
111
117
  }
112
- if (this.cfg.l3.enabled && distillOn) {
118
+ if (cfg.l3.enabled && distillOn) {
113
119
  for (const family of ['chat', 'work']) {
114
120
  try {
115
- await runPersona(this.ctx, this.cfg, this.stores.scenes[family], this.stores.persona[family], this.states[family], this.logger, family);
121
+ await runPersona(this.ctx, cfg, this.stores.scenes[family], this.stores.persona[family], this.states[family], this.logger, family);
116
122
  }
117
123
  catch (err) {
118
124
  this.logger.warn(`[memory] L3 画像蒸馏失败(family=${family}): ${errDetail(err)}`);
@@ -6,6 +6,8 @@
6
6
  import type { Context } from '@deepseek-ai/cordis';
7
7
  import Schema from '@deepseek-ai/schemastery';
8
8
  import type { MemoryLogger } from './types.js';
9
+ /** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
10
+ export type EffortChoice = '' | 'off' | 'high' | 'max';
9
11
  export interface MemoryLiveSettings {
10
12
  /** 总开关:关 = 捕获/蒸馏/召回注入全停(数据保留) */
11
13
  enabled: boolean;
@@ -15,6 +17,8 @@ export interface MemoryLiveSettings {
15
17
  distill: boolean;
16
18
  /** 召回注入(画像/记忆上下文) */
17
19
  recall: boolean;
20
+ /** 蒸馏思考档位运行时覆盖:'' = 跟随静态 config(llm.reasoningEffort) */
21
+ reasoningEffort: EffortChoice;
18
22
  }
19
23
  export interface LiveSettingsHandle {
20
24
  /** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
package/dist/settings.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import Schema from '@deepseek-ai/schemastery';
2
2
  import { settingsNamespace } from '@deepseek-ai/dsh-settings';
3
3
  const NS = settingsNamespace('dsh-memory');
4
- const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true };
4
+ const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' };
5
5
  export function liveSettingsSchema() {
6
6
  return Schema.object({
7
7
  enabled: Schema.boolean().default(true),
8
8
  capture: Schema.boolean().default(true),
9
9
  distill: Schema.boolean().default(true),
10
10
  recall: Schema.boolean().default(true),
11
+ reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
11
12
  });
12
13
  }
13
14
  export function registerLiveSettings(ctx, logger) {
@@ -27,7 +28,8 @@ export function registerLiveSettings(ctx, logger) {
27
28
  scope.watch((next) => {
28
29
  const prev = current;
29
30
  current = resolveSettings(next);
30
- logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}(此前 总=${prev.enabled})`);
31
+ logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
32
+ `,蒸馏思考=${current.reasoningEffort || '跟随配置'}(此前 总=${prev.enabled})`);
31
33
  });
32
34
  inner = {
33
35
  supported: true,
@@ -36,7 +38,8 @@ export function registerLiveSettings(ctx, logger) {
36
38
  await scope.update(patch);
37
39
  },
38
40
  };
39
- logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall})`);
41
+ logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
42
+ `,蒸馏思考=${current.reasoningEffort || '跟随配置'})`);
40
43
  return true;
41
44
  }
42
45
  catch (err) {
@@ -64,10 +67,14 @@ function resolveSettings(value) {
64
67
  if (!value || typeof value !== 'object')
65
68
  return { ...ALWAYS_ON };
66
69
  const v = value;
70
+ const efforts = ['', 'off', 'high', 'max'];
67
71
  return {
68
72
  enabled: v.enabled !== false,
69
73
  capture: v.capture !== false,
70
74
  distill: v.distill !== false,
71
75
  recall: v.recall !== false,
76
+ reasoningEffort: typeof v.reasoningEffort === 'string' && efforts.includes(v.reasoningEffort)
77
+ ? v.reasoningEffort
78
+ : '',
72
79
  };
73
80
  }
package/dist/stats.js CHANGED
@@ -130,26 +130,41 @@ async function handleEndpoint(endpoint, payload, deps) {
130
130
  deps.logger.info(`[memory] 会话档位设置 session=${p.sessionId} mode=${p.mode}`);
131
131
  return { sessionId: p.sessionId, mode: p.mode };
132
132
  }
133
- case 'dsh-memory/settings-get':
133
+ case 'dsh-memory/settings-get': {
134
+ const s = live?.get();
134
135
  return {
135
136
  supported: live?.supported ?? false,
136
- settings: live?.get() ?? { enabled: true, capture: true, distill: true, recall: true },
137
+ settings: s ?? { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' },
137
138
  // 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
138
139
  ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
140
+ // 蒸馏思考档位:current 是运行时覆盖('' = 跟随配置),effective 是实际生效值
141
+ effort: {
142
+ current: s?.reasoningEffort ?? '',
143
+ effective: s?.reasoningEffort || cfg.llm.reasoningEffort,
144
+ fallback: cfg.llm.reasoningEffort,
145
+ },
139
146
  };
147
+ }
140
148
  case 'dsh-memory/settings-set': {
141
149
  if (!live)
142
150
  throw new Error('开关通道未初始化');
143
151
  const patch = (payload ?? {});
144
- const allowed = ['enabled', 'capture', 'distill', 'recall'];
145
152
  const clean = {};
146
- for (const key of allowed) {
153
+ for (const key of ['enabled', 'capture', 'distill', 'recall']) {
147
154
  if (typeof patch[key] === 'boolean')
148
155
  clean[key] = patch[key];
149
156
  }
157
+ if (patch.reasoningEffort !== undefined) {
158
+ const v = String(patch.reasoningEffort);
159
+ if (!['', 'off', 'high', 'max'].includes(v)) {
160
+ throw new Error(`非法思考档位: ${v}(允许 ''/off/high/max)`);
161
+ }
162
+ clean.reasoningEffort = v;
163
+ }
150
164
  if (Object.keys(clean).length === 0)
151
165
  throw new Error('开关更新载荷为空');
152
166
  await live.update(clean);
167
+ deps.logger.info(`[memory] 设置更新:${JSON.stringify(clean)}`);
153
168
  return { ok: true, settings: live.get() };
154
169
  }
155
170
  case 'dsh-memory/list-records': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-layered-memory",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",