dsh-memoir 0.6.1 → 0.6.2

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 CHANGED
@@ -12,10 +12,10 @@
12
12
  No embeddings, vector database, or cloud memory service. The npm package has zero bundled runtime dependencies; DSH peers are supplied by the host.
13
13
 
14
14
  > [!IMPORTANT]
15
- > npm `latest` is `dsh-memoir@0.6.1` for `@deepseek-ai/dsh >=0.1.2-alpha.2 <0.1.3`, validated against DSH alpha.4 and the current alpha.5. Users remaining on `0.1.1-rc.2` should pin `dsh-memoir@0.5.6`.
15
+ > npm `latest` is `dsh-memoir@0.6.2` for `@deepseek-ai/dsh >=0.1.2-alpha.2 <0.1.3`, validated against DSH alpha.4, alpha.5, and 0.1.2-rc.1. Users remaining on `0.1.1-rc.2` should pin `dsh-memoir@0.5.6`.
16
16
 
17
17
  ```bash
18
- npm install --global @deepseek-ai/dsh@alpha
18
+ npm install --global @deepseek-ai/dsh@0.1.2-rc.1
19
19
  dsh plugin --profile web add dsh-memoir@latest
20
20
  ```
21
21
 
@@ -77,6 +77,10 @@ Similar-memory governance starts with BM25 candidates, then combines title simil
77
77
 
78
78
  ## Automatic distillation
79
79
 
80
+ v0.6.2 diagnostics show the latest decision and process-local counters. Turns calling `memoir_record` or `memoir_update` skip reminders; submitting a reminder does not confirm persistence. Agent disposal clears gate state, with a fallback cap of 1024 recently active agents (eviction also forgets their turn watermark and cooldown). Manual recording remains available when automatic distillation is disabled.
81
+
82
+ The current validated baseline is DSH `0.1.2-rc.1`. The next `0.1.3-alpha.2` line is not yet declared compatible; use the pinned installation command. BM25 is lexical retrieval and does not guarantee cross-language semantic matches without shared terms. Distillation guidance does not replace fact checking.
83
+
80
84
  Automatic distillation is an observable agent turn-end reminder, not silent background scraping of every chat. The default `1 / 0 / 1` means every eligible worked turn, no extra cooldown, and at least one tool call.
81
85
 
82
86
  `autoDistillEvery`, `autoDistillCooldownMin`, and `autoDistillMinTools` are AND conditions isolated per agent. Idle, aborted, subagent, and already-recorded turns do not trigger. Cooldown advances only after a successful reminder. All cadence parameters are live-editable in the GUI.
@@ -113,6 +117,8 @@ Installing into a DSH-alpha `web` profile registers a native Memory Conversation
113
117
  <details>
114
118
  <summary>More GUI screenshots</summary>
115
119
 
120
+ ![v0.6.2 automatic distillation lifecycle diagnostics](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.2/picture/v0.6.2-distill-diagnostics-zh.png)
121
+
116
122
  ![v0.6.1 permanent surface navigation and live settings](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.1/picture/v0.6.1-settings-navigation-zh.png)
117
123
 
118
124
  ![v0.6.1 conversation view scrolled to the end above the composer](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.1/picture/v0.6.1-conversation-scroll-zh.png)
@@ -131,9 +137,9 @@ Installing into a DSH-alpha `web` profile registers a native Memory Conversation
131
137
 
132
138
  | Channel | DSH baseline | Installation | Status |
133
139
  | --- | --- | --- | --- |
134
- | npm `latest` (`0.6.1`) | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@latest` | Current release; alpha.4 compile and alpha.5 + dsh-web-all browser validated |
140
+ | npm `latest` (`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@latest` | Current release validated on 0.1.2-rc.1 |
135
141
  | pinned npm `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 compatibility line |
136
- | GitHub `main` (`0.6.1`) | `>=0.1.2-alpha.2 <0.1.3` | source clone + `link:` | Synchronized with npm `0.6.1`; intended for development and debugging |
142
+ | GitHub `main` (`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | source clone + `link:` | Development and debugging |
137
143
 
138
144
  Node.js `^22.19.0 || >=24.0.0` is required. v0.6.x uses the native DSH-alpha `conversation.view` / `settings.section` slots and the Remote-era client module architecture. v0.6.1 supports both the public `session.events` surface in alpha.2/alpha.3 and `session.snapshotEvents()` in alpha.4+. `dsh.engines.dsh` rejects incompatible hosts.
139
145
 
@@ -147,7 +153,7 @@ git clone https://github.com/Qinling-Melon-Farmers/dsh-memoir.git
147
153
  cd dsh-memoir
148
154
  pnpm install --frozen-lockfile
149
155
  pnpm run build
150
- npm install --global @deepseek-ai/dsh@alpha
156
+ npm install --global @deepseek-ai/dsh@0.1.2-rc.1
151
157
  dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
152
158
  ```
153
159
 
@@ -204,7 +210,7 @@ v0.5.6 benchmark (Node 24.19, 900/1200-token budget; full data in [`bench/report
204
210
 
205
211
  Numbers vary by machine and corpus. The important properties are that injection remains bounded and the cache-hit path is independent of total memory size.
206
212
 
207
- v0.6.1 has 189 automated tests covering store/settings migration and locks, Hot Memory, BM25 quality/cache, lifecycle, provenance anti-spoofing, similar-memory governance, automatic distillation, bilingual agent and GUI surfaces, project disclosure/progressive loading, scrolling, and DSH-alpha compatibility. An isolated DSH alpha.5 + `@linxin666/dsh-web-all@0.3.12` profile also passed Settings and real-session browser regression; alpha.4 type compilation passed separately.
213
+ v0.6.2 passes 192 automated tests and type checking, covering lifecycle, BM25, Hot Memory, writing and injection, provenance, and GUI. An isolated DSH 0.1.2-rc.1 + dsh-web-all 0.3.18 profile passes bilingual diagnostics browser regression. Earlier alpha.2–alpha.5 compatibility evidence is recorded in the v0.6.1 changelog.
208
214
 
209
215
  ## FAQ
210
216
 
@@ -233,6 +239,6 @@ pnpm test
233
239
  npm run bench
234
240
  ```
235
241
 
236
- Read [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting changes. See [CHANGELOG.md](./CHANGELOG.md) for version history. Formal packages are published by the tag workflow through npm OIDC. The current npm release is [v0.6.1](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.6.1), and `main` is synchronized with it.
242
+ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before submitting changes. See [CHANGELOG.md](./CHANGELOG.md) for version history. Formal packages are published by the tag workflow through npm OIDC. The current npm release is [v0.6.2](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.6.2), and `main` is synchronized with it.
237
243
 
238
244
  Apache-2.0
package/README.md CHANGED
@@ -11,10 +11,10 @@
11
11
  无需 embedding、向量数据库或云端记忆服务;npm 包零捆绑运行时依赖,DSH peer 由宿主提供。
12
12
 
13
13
  > [!IMPORTANT]
14
- > npm `latest` 为 `dsh-memoir@0.6.1`,适用于 `@deepseek-ai/dsh >=0.1.2-alpha.2 <0.1.3`,已验证 DSH alpha.4 与当前 alpha.5。仍使用 `0.1.1-rc.2` 的用户应固定安装 `dsh-memoir@0.5.6`。
14
+ > npm `latest` 为 `dsh-memoir@0.6.2`,适用于 `@deepseek-ai/dsh >=0.1.2-alpha.2 <0.1.3`,已验证 DSH alpha.4、alpha.5 与 0.1.2-rc.1。仍使用 `0.1.1-rc.2` 的用户应固定安装 `dsh-memoir@0.5.6`。
15
15
 
16
16
  ```bash
17
- npm install --global @deepseek-ai/dsh@alpha
17
+ npm install --global @deepseek-ai/dsh@0.1.2-rc.1
18
18
  dsh plugin --profile web add dsh-memoir@latest
19
19
  ```
20
20
 
@@ -76,6 +76,10 @@ memoir_record / memoir_update
76
76
 
77
77
  ## 自动蒸馏
78
78
 
79
+ v0.6.2 的诊断页显示最近触发或跳过原因及本次进程计数。已经调用 `memoir_record` 或 `memoir_update` 的回合不再提醒;提交提醒不代表写入已完成。Agent 销毁会清理门控状态,最多保留 1024 个最近活动 Agent(淘汰后不再保留其回合水位和冷却)。关闭自动蒸馏后仍可手动记录。
80
+
81
+ 当前验证基线为 DSH `0.1.2-rc.1`;`0.1.3-alpha.2` 属于下一版本线,尚不声明兼容,请使用下方固定版本安装命令。BM25 是词项召回,不能保证无共同词项的跨语言语义匹配;提炼质量提示也不能替代事实核验。
82
+
79
83
  自动蒸馏是可观察的 Agent 收尾提醒,不是后台静默抓取聊天内容。默认 `1 / 0 / 1` 表示:每个有效 worked turn、无额外冷却、至少一次工具调用即可提醒。
80
84
 
81
85
  `autoDistillEvery`、`autoDistillCooldownMin`、`autoDistillMinTools` 三个条件按 AND 判定并按 Agent 隔离。idle、aborted、subagent 和已调用 `memoir_record` 的回合不会触发;冷却只在提醒成功后更新。所有频率参数都可在 GUI 中即时修改。
@@ -112,6 +116,8 @@ memoir_record / memoir_update
112
116
  <details>
113
117
  <summary>查看更多 GUI 截图</summary>
114
118
 
119
+ ![v0.6.2 自动蒸馏生命周期诊断](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.2/picture/v0.6.2-distill-diagnostics-zh.png)
120
+
115
121
  ![v0.6.1 常驻功能导航与实时设置](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.1/picture/v0.6.1-settings-navigation-zh.png)
116
122
 
117
123
  ![v0.6.1 对话视图滚动到底且避让输入框](https://raw.githubusercontent.com/Qinling-Melon-Farmers/dsh-memoir/v0.6.1/picture/v0.6.1-conversation-scroll-zh.png)
@@ -130,9 +136,9 @@ memoir_record / memoir_update
130
136
 
131
137
  | 渠道 | DSH 基线 | 安装方式 | 状态 |
132
138
  | --- | --- | --- | --- |
133
- | npm `latest`(`0.6.1`) | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@latest` | 当前正式版;已验证 alpha.4 编译及 alpha.5 + dsh-web-all 实机 UI |
139
+ | npm `latest`(`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@latest` | 当前发布版本;基于 0.1.2-rc.1 验证 |
134
140
  | npm 固定版 `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 兼容线 |
135
- | GitHub `main`(`0.6.1`) | `>=0.1.2-alpha.2 <0.1.3` | 源码 clone + `link:` | 与 npm `0.6.1` 同步;供开发和调试使用 |
141
+ | GitHub `main`(`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | 源码 clone + `link:` | 开发和调试使用 |
136
142
 
137
143
  需要 Node.js `^22.19.0 || >=24.0.0`。`0.6.x` 使用 DSH alpha 的原生 `conversation.view` / `settings.section` 与 Remote 时代客户端模块;`0.6.1` 同时兼容 alpha.2/alpha.3 的公开 `session.events` 与 alpha.4+ 的 `session.snapshotEvents()`。manifest 的 `dsh.engines.dsh` 会拒绝不兼容宿主。
138
144
 
@@ -146,7 +152,7 @@ git clone https://github.com/Qinling-Melon-Farmers/dsh-memoir.git
146
152
  cd dsh-memoir
147
153
  pnpm install --frozen-lockfile
148
154
  pnpm run build
149
- npm install --global @deepseek-ai/dsh@alpha
155
+ npm install --global @deepseek-ai/dsh@0.1.2-rc.1
150
156
  dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
151
157
  ```
152
158
 
@@ -203,7 +209,7 @@ v0.5.6 基准(Node 24.19,900/1200 token;完整数据见 [`bench/report.md`
203
209
 
204
210
  基准值取决于机器和语料;它证明的重点是注入预算保持有界、缓存命中路径与记忆总量解耦。
205
211
 
206
- v0.6.1 有 189 项自动化测试,覆盖存储/设置迁移与锁、Hot Memory、BM25 质量/缓存、生命周期、来源防伪、相似治理、自动蒸馏、Agent 侧与 GUI 双语、项目折叠/渐进加载、滚动布局及 DSH alpha 兼容。另在隔离 profile 中以 DSH alpha.5 + `@linxin666/dsh-web-all@0.3.12` 完成 Settings 与真实会话页浏览器回归;alpha.4 类型编译也已通过。
212
+ v0.6.2 的 192 项自动化测试与类型检查通过,覆盖生命周期、BM25、Hot Memory、写入与注入、来源和 GUI。当前隔离 DSH 0.1.2-rc.1 + dsh-web-all 0.3.18 已完成双语诊断页浏览器回归;alpha.2–alpha.5 的旧版兼容证据见 v0.6.1 更新日志。
207
213
 
208
214
  ## 常见问题
209
215
 
@@ -232,6 +238,6 @@ pnpm test
232
238
  npm run bench
233
239
  ```
234
240
 
235
- 提交前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。版本变化见 [CHANGELOG.md](./CHANGELOG.md),正式包由 tag 工作流通过 npm OIDC 发布。当前 npm 正式版是 [v0.6.1](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.6.1),`main` 与该版本同步。
241
+ 提交前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。版本变化见 [CHANGELOG.md](./CHANGELOG.md),正式包由 tag 工作流通过 npm OIDC 发布。当前 npm 正式版是 [v0.6.2](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.6.2),`main` 与该版本同步。
236
242
 
237
243
  Apache-2.0
@@ -62,6 +62,10 @@ export interface AutoDistillPolicy {
62
62
  /** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
63
63
  export declare class AutoDistillGate {
64
64
  private states;
65
+ readonly capacity = 1024;
66
+ reason: 'duplicate' | 'interval' | 'tools' | 'cooldown' | 'ready';
67
+ get size(): number;
68
+ clear(): void;
65
69
  /**
66
70
  * Consume one eligible worked turn and decide whether all policy conditions
67
71
  * are ready. Duplicate events never advance the worked-turn counter.
@@ -72,6 +76,39 @@ export declare class AutoDistillGate {
72
76
  /** Drop all state for one agent (disposal hygiene). */
73
77
  forget(agentId: string): void;
74
78
  }
79
+ export type DistillOutcome = 'disabled' | 'subagent' | 'aborted' | 'idle' | 'recorded' | 'duplicate' | 'interval' | 'tools' | 'cooldown' | 'steered' | 'failed';
80
+ /** Process-local counters only; never retains message content or credentials. */
81
+ export declare class DistillDiagnostics {
82
+ private counts;
83
+ private last;
84
+ private workedTurns;
85
+ private agents;
86
+ record(outcome: DistillOutcome, at: number, turn: number, toolCalls: number, agents: number): void;
87
+ snapshot(): {
88
+ counts: {
89
+ subagent?: number | undefined;
90
+ duplicate?: number | undefined;
91
+ interval?: number | undefined;
92
+ tools?: number | undefined;
93
+ cooldown?: number | undefined;
94
+ disabled?: number | undefined;
95
+ aborted?: number | undefined;
96
+ idle?: number | undefined;
97
+ recorded?: number | undefined;
98
+ steered?: number | undefined;
99
+ failed?: number | undefined;
100
+ };
101
+ workedTurns: number;
102
+ agents: number;
103
+ last: {
104
+ outcome: DistillOutcome;
105
+ at: number;
106
+ turn: number;
107
+ toolCalls: number;
108
+ } | null;
109
+ };
110
+ setAgents(agents: number): void;
111
+ }
75
112
  export interface TurnStoppingPayload {
76
113
  agent: AutoDistillAgentLike;
77
114
  turn: number;
@@ -80,6 +117,7 @@ export interface TurnStoppingPayload {
80
117
  /** The event-wire surface the installer needs (satisfied by ctx.on). */
81
118
  export interface AutoDistillWire {
82
119
  on(name: 'agent/turn-stopping', listener: (payload: TurnStoppingPayload) => void): () => void;
120
+ onDisposed?(listener: (agentId: string) => void): () => void;
83
121
  }
84
122
  /**
85
123
  * Install the turn-end listener. The returned disposer removes the listener.
@@ -100,4 +138,5 @@ export declare function installAutoDistill(wire: AutoDistillWire, options: {
100
138
  /** Optional live language source for the steering instruction. */
101
139
  language?: () => MemoirLanguage;
102
140
  now?: () => number;
141
+ diagnostics?: DistillDiagnostics;
103
142
  }): () => void;
@@ -40,7 +40,7 @@ export function turnActivity(events, turn) {
40
40
  continue;
41
41
  if (event.type === 'tool/call') {
42
42
  toolCalls += 1;
43
- if (data.name === 'memoir_record')
43
+ if (data.name === 'memoir_record' || data.name === 'memoir_update')
44
44
  recorded = true;
45
45
  }
46
46
  }
@@ -53,6 +53,10 @@ export function isSubagentSession(agent) {
53
53
  /** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
54
54
  export class AutoDistillGate {
55
55
  states = new Map();
56
+ capacity = 1024;
57
+ reason = 'ready';
58
+ get size() { return this.states.size; }
59
+ clear() { this.states.clear(); }
56
60
  /**
57
61
  * Consume one eligible worked turn and decide whether all policy conditions
58
62
  * are ready. Duplicate events never advance the worked-turn counter.
@@ -60,21 +64,24 @@ export class AutoDistillGate {
60
64
  consume(agentId, turn, toolCalls, policy, now) {
61
65
  let state = this.states.get(agentId);
62
66
  if (state === undefined) {
63
- state = { processedTurns: new Set(), workedSinceSteer: 0 };
67
+ state = { lastTurn: -Infinity, workedSinceSteer: 0 };
64
68
  this.states.set(agentId, state);
69
+ if (this.states.size > this.capacity)
70
+ this.states.delete(this.states.keys().next().value);
65
71
  }
66
- if (state.processedTurns.has(turn))
72
+ if (turn <= state.lastTurn) {
73
+ this.reason = 'duplicate';
67
74
  return false;
68
- state.processedTurns.add(turn);
69
- for (const value of [...state.processedTurns]) {
70
- if (value < turn - 100)
71
- state.processedTurns.delete(value);
72
75
  }
76
+ state.lastTurn = turn;
77
+ this.states.delete(agentId);
78
+ this.states.set(agentId, state);
73
79
  state.workedSinceSteer += 1;
74
80
  const intervalReady = state.workedSinceSteer >= policy.every;
75
81
  const activityReady = toolCalls >= policy.minTools;
76
82
  const cooldownReady = state.lastSteeredAt === undefined || now - state.lastSteeredAt >= policy.cooldownMs;
77
- return intervalReady && activityReady && cooldownReady;
83
+ this.reason = !intervalReady ? 'interval' : !activityReady ? 'tools' : !cooldownReady ? 'cooldown' : 'ready';
84
+ return this.reason === 'ready';
78
85
  }
79
86
  /** Record a successful steer; failed steer attempts do not start cooldown. */
80
87
  recordSteer(agentId, now) {
@@ -89,6 +96,22 @@ export class AutoDistillGate {
89
96
  this.states.delete(agentId);
90
97
  }
91
98
  }
99
+ /** Process-local counters only; never retains message content or credentials. */
100
+ export class DistillDiagnostics {
101
+ counts = {};
102
+ last = null;
103
+ workedTurns = 0;
104
+ agents = 0;
105
+ record(outcome, at, turn, toolCalls, agents) {
106
+ this.counts[outcome] = (this.counts[outcome] ?? 0) + 1;
107
+ if (['interval', 'tools', 'cooldown', 'steered', 'failed'].includes(outcome))
108
+ this.workedTurns += 1;
109
+ this.last = { outcome, at, turn, toolCalls };
110
+ this.agents = agents;
111
+ }
112
+ snapshot() { return { counts: { ...this.counts }, workedTurns: this.workedTurns, agents: this.agents, last: this.last === null ? null : { ...this.last } }; }
113
+ setAgents(agents) { this.agents = agents; }
114
+ }
92
115
  /**
93
116
  * Install the turn-end listener. The returned disposer removes the listener.
94
117
  * @param wire - the event wire (the cordis context).
@@ -98,30 +121,50 @@ export function installAutoDistill(wire, options) {
98
121
  const gate = new AutoDistillGate();
99
122
  const integerAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : fallback;
100
123
  const numberAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, value) : fallback;
101
- return wire.on('agent/turn-stopping', (payload) => {
102
- if (!options.enabled())
103
- return;
124
+ const dispose = wire.on('agent/turn-stopping', (payload) => {
104
125
  const { agent, turn, signal } = payload;
105
- if (isSubagentSession(agent))
126
+ const now = options.now?.() ?? Date.now();
127
+ const report = (outcome, tools = 0) => options.diagnostics?.record(outcome, now, turn, tools, gate.size);
128
+ if (!options.enabled()) {
129
+ report('disabled');
130
+ return;
131
+ }
132
+ if (isSubagentSession(agent)) {
133
+ report('subagent');
106
134
  return;
107
- if (signal.aborted)
135
+ }
136
+ if (signal.aborted) {
137
+ report('aborted');
108
138
  return;
139
+ }
109
140
  const { worked, recorded, toolCalls } = turnActivity(sessionEventSnapshot(agent.session), turn);
110
- if (!worked || recorded)
141
+ if (!worked || recorded) {
142
+ report(recorded ? 'recorded' : 'idle', toolCalls);
111
143
  return;
144
+ }
112
145
  const live = options.policy?.();
113
146
  const policy = {
114
147
  every: integerAtLeast(live?.every ?? options.every, 1, 1),
115
148
  cooldownMs: numberAtLeast(live?.cooldownMin ?? options.cooldownMin, 0, 0) * 60_000,
116
149
  minTools: integerAtLeast(live?.minTools ?? options.minTools, 1, 1),
117
150
  };
118
- const now = options.now?.() ?? Date.now();
119
- if (!gate.consume(agent.id, turn, toolCalls, policy, now))
151
+ if (!gate.consume(agent.id, turn, toolCalls, policy, now)) {
152
+ report(gate.reason === 'ready' ? 'duplicate' : gate.reason, toolCalls);
120
153
  return;
121
- agent.steer(createUserMessage({
122
- content: [{ type: 'text', text: distillPrompt(options.language?.()) }],
123
- source: { kind: 'plugin', plugin: AUTO_DISTILL_PLUGIN },
124
- }));
154
+ }
155
+ try {
156
+ agent.steer(createUserMessage({
157
+ content: [{ type: 'text', text: distillPrompt(options.language?.()) }],
158
+ source: { kind: 'plugin', plugin: AUTO_DISTILL_PLUGIN },
159
+ }));
160
+ }
161
+ catch (error) {
162
+ report('failed', toolCalls);
163
+ throw error;
164
+ }
125
165
  gate.recordSteer(agent.id, now);
166
+ report('steered', toolCalls);
126
167
  });
168
+ const disposeAgent = wire.onDisposed?.((agentId) => { gate.forget(agentId); options.diagnostics?.setAgents(gate.size); });
169
+ return () => { dispose(); disposeAgent?.(); gate.clear(); options.diagnostics?.setAgents(0); };
127
170
  }
package/lib/client.js CHANGED
@@ -157,6 +157,22 @@ var MemoirApi = class {
157
157
  // src/client/i18n.ts
158
158
  var dictionaries = {
159
159
  zh: {
160
+ "distill.title": "\u81EA\u52A8\u84B8\u998F\u751F\u547D\u5468\u671F",
161
+ "distill.worked": "\u7D2F\u8BA1\u6709\u6548\u5DE5\u4F5C\u56DE\u5408",
162
+ "distill.agents": "\u8DDF\u8E2A Agent",
163
+ "distill.note": "\u672C\u6B21\u8FDB\u7A0B\u5185\u7EDF\u8BA1\uFF1B\u63D0\u9192\u6210\u529F\u4E0D\u4EE3\u8868\u8BB0\u5FC6\u5DF2\u5199\u5165\u3002\u505C\u7528\u81EA\u52A8\u84B8\u998F\u540E\u4ECD\u53EF\u624B\u52A8\u8BB0\u5F55\u3002",
164
+ "distill.last": "\u6700\u8FD1\u51B3\u7B56",
165
+ "distill.disabled": "\u81EA\u52A8\u84B8\u998F\u5DF2\u5173\u95ED",
166
+ "distill.subagent": "\u5B50\u4EE3\u7406\u8DF3\u8FC7",
167
+ "distill.aborted": "\u4E2D\u6B62\u8DF3\u8FC7",
168
+ "distill.idle": "\u65E0\u5DE5\u5177\u6D3B\u52A8",
169
+ "distill.recorded": "\u5DF2\u8C03\u7528\u8BB0\u5FC6\u5199\u5165\u6216\u66F4\u65B0",
170
+ "distill.duplicate": "\u91CD\u590D\u6216\u8FC7\u671F\u56DE\u5408",
171
+ "distill.interval": "\u672A\u8FBE\u56DE\u5408\u95F4\u9694",
172
+ "distill.tools": "\u672A\u8FBE\u5DE5\u5177\u8C03\u7528\u9608\u503C",
173
+ "distill.cooldown": "\u51B7\u5374\u4E2D",
174
+ "distill.steered": "\u63D0\u9192\u5DF2\u63D0\u4EA4",
175
+ "distill.failed": "\u63D0\u9192\u63D0\u4EA4\u5931\u8D25",
160
176
  "entry.label": "\u8BB0\u5FC6",
161
177
  "entry.tooltip": "\u6253\u5F00\u8BB0\u5FC6\u9762\u677F\uFF08\u9879\u76EE\u8BB0\u5FC6 / \u5168\u5C40\u8BB0\u5FC6\uFF09",
162
178
  "panel.title": "\u8BB0\u5FC6 Memoir",
@@ -319,6 +335,22 @@ var dictionaries = {
319
335
  "settings.saveFailed": "\u8BBE\u7F6E\u4FDD\u5B58\u5931\u8D25"
320
336
  },
321
337
  en: {
338
+ "distill.title": "Automatic distillation lifecycle",
339
+ "distill.worked": "Eligible worked turns",
340
+ "distill.agents": "Tracked agents",
341
+ "distill.note": "Process-local counters; a submitted reminder does not confirm a memory write. Manual recording remains available when disabled.",
342
+ "distill.last": "Latest decision",
343
+ "distill.disabled": "Automatic distillation disabled",
344
+ "distill.subagent": "Subagent skipped",
345
+ "distill.aborted": "Aborted turn skipped",
346
+ "distill.idle": "No tool activity",
347
+ "distill.recorded": "Memory write or update already called",
348
+ "distill.duplicate": "Duplicate or stale turn",
349
+ "distill.interval": "Turn interval not reached",
350
+ "distill.tools": "Tool threshold not reached",
351
+ "distill.cooldown": "Cooldown active",
352
+ "distill.steered": "Reminder submitted",
353
+ "distill.failed": "Reminder submission failed",
322
354
  "entry.label": "Memory",
323
355
  "entry.tooltip": "Open the memory panel (project / global)",
324
356
  "panel.title": "Memory Memoir",
@@ -1426,6 +1458,34 @@ ${projectSignature}`;
1426
1458
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { id: `${panelId}-diagnostics-panel`, className: "memoir-surface", role: "tabpanel", "aria-labelledby": `${panelId}-diagnostics-tab`, hidden: surface !== "diagnostics", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-surface-scroll", "data-dsh-part": "diagnostics-scroll", tabIndex: 0, "aria-label": t("surface.diagnostics"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-surface-card", "data-dsh-part": "diagnostics", children: [
1427
1459
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: t("diag.title") }),
1428
1460
  diag === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "memoir-empty", children: t("diag.unavailable") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "memoir-diagnostics-body", children: [
1461
+ diag.autoDistill ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { children: t("distill.title") }),
1463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1464
+ t("distill.worked"),
1465
+ ": ",
1466
+ diag.autoDistill.workedTurns,
1467
+ " \xB7 ",
1468
+ t("distill.agents"),
1469
+ ": ",
1470
+ diag.autoDistill.agents,
1471
+ "/1024"
1472
+ ] }),
1473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: t("distill.note") }),
1474
+ Object.entries(diag.autoDistill.counts).map(([reason, count]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1475
+ t(`distill.${reason}`),
1476
+ ": ",
1477
+ count
1478
+ ] }, reason)),
1479
+ diag.autoDistill.last ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1480
+ t("distill.last"),
1481
+ ": ",
1482
+ t(`distill.${diag.autoDistill.last.outcome}`),
1483
+ " \xB7 ",
1484
+ new Date(diag.autoDistill.last.at).toLocaleString(),
1485
+ " \xB7 turn ",
1486
+ diag.autoDistill.last.turn
1487
+ ] }) : null
1488
+ ] }) : null,
1429
1489
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1430
1490
  t("diag.revision"),
1431
1491
  ": ",