dsh-memoir 0.8.1 → 0.8.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,13 +12,13 @@
12
12
  No embeddings, vector database, or cloud memory service. The npm package has zero bundled runtime dependencies; DSH and Zod 4 peers are supplied by the host environment; Zod validates session projections and is not bundled.
13
13
 
14
14
  > [!IMPORTANT]
15
- > **0.8.1 requires DSH `>=0.1.7-rc.1 <0.1.8-0`**, with SDKs pinned to `0.1.7-rc.2`. It refines unskinned Desktop settings backgrounds, spacing and cards, adds actual-save diagnostics, and adopts public Session projections. Keep `dsh-memoir@0.7.1` on DSH 0.1.5.
15
+ > **0.8.2 requires DSH `>=0.1.7-rc.1 <0.1.8-0`**, with SDKs pinned to `0.1.7-rc.2`. It fixes auto-distillation hiding the original task answer, adds an optional native right-sidebar panel and offline About & help, and retains unskinned Desktop styling, actual-save diagnostics and public Session projections. Keep `dsh-memoir@0.7.1` on DSH 0.1.5.
16
16
  >
17
17
  > `dsh-memoir@0.7.1` fixes lost session snapshots after restart or RAM eviction (#10), supporting DSH **0.1.5-rc.1 / rc.2**. It requires `>=0.1.5-rc.1 <0.1.6-0`; check your host first. Keep `0.6.2` on DSH 0.1.2, or `0.5.6` on DSH 0.1.1-rc.2; those older lines do not include this fix.
18
18
 
19
19
  ```bash
20
20
  npm install --global @deepseek-ai/dsh@0.1.7-rc.2
21
- dsh plugin --profile web add dsh-memoir@0.8.1
21
+ dsh plugin --profile web add dsh-memoir@0.8.2
22
22
  ```
23
23
 
24
24
  Restart `dsh web`. Memory remains local and is not automatically deleted when the plugin is updated or removed.
@@ -88,12 +88,16 @@ Similar-memory governance starts with BM25 candidates, then combines title simil
88
88
 
89
89
  ## Automatic distillation
90
90
 
91
- 0.8.1 diagnostics distinguish reminders, persistence, failures, cancellation, unresolved similarity, and unavailable receipts. Only persisted `memoir_record` / `memoir_update` operations suppress reminders; failed calls or unresolved candidates do not count as saves. Host cancellation may follow persistence, so counters can overlap. Correlation with a reminder proves neither causation nor semantic correctness. GUI writes are excluded from Agent-tool counters.
91
+ 0.8.2 diagnostics distinguish reminders, persistence, failures, cancellation, unresolved similarity, and unavailable receipts. Only persisted `memoir_record` / `memoir_update` operations suppress reminders; failed calls or unresolved candidates do not count as saves. Host cancellation may follow persistence, so counters can overlap. Correlation with a reminder proves neither causation nor semantic correctness. GUI writes are excluded from Agent-tool counters.
92
92
 
93
- 0.8.1 uses public `sessionProjections` to reconstruct current-turn activity from logs and checkpoints without deprecated `snapshotEvents()` reads. Each session retains at most 4096 current-turn call IDs, without arguments or content; evicted IDs degrade to session-only provenance. Gate state is capped at 1024 agents and cleared on disposal. Legacy writes have no receipts: a historical call alone cannot prove persistence. BM25 remains lexical retrieval, not guaranteed cross-language semantic matching.
93
+ 0.8.2 uses public `sessionProjections` to reconstruct current-turn activity from logs and checkpoints without deprecated `snapshotEvents()` reads. Each session retains at most 4096 current-turn call IDs, without arguments or content; evicted IDs degrade to session-only provenance. Gate state is capped at 1024 agents and cleared on disposal. Legacy writes have no receipts: a historical call alone cannot prove persistence. BM25 remains lexical retrieval, not guaranteed cross-language semantic matching.
94
94
 
95
95
  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.
96
96
 
97
+ > **0.8.2 fixes distillation-induced answer folding for new turns ([#13](https://github.com/Qinling-Melon-Farmers/dsh-memoir/issues/13)):** The public `followup` API queues a separate memory wrap-up turn instead of using same-turn `steer`. A short receipt, empty output or failed wrap-up tool cannot change the original task answer’s turn boundary. Memory provenance still points to the source work turn. Wrap-up turns neither advance the worked-turn cadence nor recursively trigger distillation. This is still visible, cancelable model work—not a free background task—and may increase the displayed turn count.
98
+ >
99
+ > Upgrading does not rewrite history. For same-turn folding already recorded by 0.8.0 / 0.8.1, expand the process disclosure or use Normal (`normal`) transcript mode. This fix does not change DSH’s answer selection for arbitrary same-turn continuations from other producers, and does not rely on a “remain silent” prompt.
100
+
97
101
  `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.
98
102
 
99
103
  `language` independently controls agent-visible tool descriptions and parameters, the distillation prompt, tool results, Hot Memory / `PROJECT_MEMORY.md` headings, and validation or governance errors. It defaults to `zh` for backward compatibility and can be switched to `en` in the GUI. Tool schemas and subsequent prompts update live without restarting DSH.
@@ -152,11 +156,11 @@ Installing into a DSH-alpha `web` profile registers a native Memory Conversation
152
156
 
153
157
  | Channel | DSH baseline | Installation | Status |
154
158
  | --- | --- | --- | --- |
155
- | npm `latest` (`0.8.1`) | `>=0.1.7-rc.1 <0.1.8-0` | `dsh plugin --profile web add dsh-memoir@0.8.1` | DSH 0.1.7 line |
159
+ | npm `latest` (`0.8.2`) | `>=0.1.7-rc.1 <0.1.8-0` | `dsh plugin --profile web add dsh-memoir@0.8.2` | DSH 0.1.7 line |
156
160
  | pinned npm `0.7.1` | `>=0.1.5-rc.1 <0.1.6-0` | `dsh plugin --profile web add dsh-memoir@0.7.1` | Legacy 0.1.5 line |
157
161
  | pinned npm `0.6.2` | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@0.6.2` | Legacy 0.1.2 line |
158
162
  | pinned npm `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 compatibility line |
159
- | Source `v0.8.1` | `>=0.1.7-rc.1 <0.1.8-0` | local build + `link:` | Development; not compatible with legacy 0.1.5 / 0.1.6 |
163
+ | Source `v0.8.2` | `>=0.1.7-rc.1 <0.1.8-0` | local build + `link:` | Development; not compatible with legacy 0.1.5 / 0.1.6 |
160
164
 
161
165
  Node.js `^22.19.0 || >=24.0.0` is required. 0.7.1 keeps the native `conversation.view` / `settings.section` slots and `snapshotEvents()`. DSH 0.1.5 uses Session log V3, independent of Memoir store v4 / settings v3. Back up DSH_HOME before upgrading DSH; migrated sessions are not guaranteed readable by older hosts. This Memoir release neither migrates nor resets memory and retains frozen session snapshots without enabling new dynamic-prompt behavior.
162
166
 
@@ -176,7 +180,14 @@ dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
176
180
 
177
181
  </details>
178
182
 
179
- 0.8.1 uses native `uiWorkspace`, `conversation.view` / `settings.section`, and the Session V4 producer-owned distillation source. Links open sessions and turn IDs remain copyable; no global DOM turn scrolling is used. Settings inherit the host background and cards use theme layers while preserving skins and independent scrolling. Store v4 / settings v3 / snapshot v1 are unchanged. Back up DSH_HOME before upgrading: host session migration is separate from plugin memory storage.
183
+ 0.8.2 uses native `uiWorkspace`, `conversation.view` / `settings.section`, and the Session V4 producer-owned distillation source. Links open sessions and turn IDs remain copyable; no global DOM turn scrolling is used. Settings inherit the host background and cards use theme layers while preserving skins and independent scrolling. Store v4 / settings v3 / snapshot v1 are unchanged. Back up DSH_HOME before upgrading: host session migration is separate from plugin memory storage.
184
+
185
+ ## Native sidebar and help
186
+
187
+ - Existing Conversation and Settings entries stay unchanged. The right-sidebar guide now offers Memory for browsing project memory, Hot Memory and diagnostics alongside chat; it never opens automatically or replaces another panel.
188
+ - Each sidebar instance follows its own session workspace and reuses the same data layer, with independent active surfaces and scroll state. Conversation and Settings remain available when the optional sidebar service is absent.
189
+ - Under Memory settings, the collapsed About & help card shows the plugin version, host range, SDK baseline, maintainer, repository, bilingual documentation, releases and issue links. The plugin repository is explicitly distinct from the current workspace.
190
+ - No background update requests, workspace Git-remote inspection or path/memory uploads. Use the host plugin manager to update, after checking the target package’s DSH requirements and prerelease channel. The panel does not auto-upgrade.
180
191
 
181
192
  ## Storage, privacy, and security boundaries
182
193
 
@@ -258,6 +269,6 @@ pnpm test
258
269
  npm run bench
259
270
  ```
260
271
 
261
- 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 version is [v0.8.1](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.8.1), targeting DSH 0.1.7. Keep 0.7.1 on legacy DSH 0.1.5.
272
+ 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 version is [v0.8.2](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.8.2), targeting DSH 0.1.7. Keep 0.7.1 on legacy DSH 0.1.5.
262
273
 
263
274
  Apache-2.0
package/README.md CHANGED
@@ -11,13 +11,13 @@
11
11
  无需 embedding、向量数据库或云端记忆服务;npm 包零捆绑运行时依赖,DSH 与 Zod 4 peer 由宿主环境提供;Zod 用于验证宿主会话投影,不捆绑进插件。
12
12
 
13
13
  > [!IMPORTANT]
14
- > **0.8.1 要求 DSH `>=0.1.7-rc.1 <0.1.8-0`**,开发 SDK 为 `0.1.7-rc.2`。优化桌面端无皮肤设置页的底色、留白及卡片排版;增加实际保存诊断,并迁移到公开 Session projection。旧 DSH 0.1.5 请固定安装 `dsh-memoir@0.7.1`。
14
+ > **0.8.2 要求 DSH `>=0.1.7-rc.1 <0.1.8-0`**,开发 SDK 为 `0.1.7-rc.2`。修复自动蒸馏遮住原任务答复的问题;新增可选原生右侧记忆面板和离线“关于与帮助”。保留无皮肤桌面适配、实际保存诊断和公开 Session projection。旧 DSH 0.1.5 请固定安装 `dsh-memoir@0.7.1`。
15
15
  >
16
16
  > `dsh-memoir@0.7.1` 修复重启和内存淘汰后旧会话快照丢失(#10),支持 DSH **0.1.5-rc.1 / rc.2**。要求 `>=0.1.5-rc.1 <0.1.6-0`;请先核对宿主版本。旧 DSH 0.1.2 用户固定使用 `0.6.2`,0.1.1-rc.2 用户固定使用 `0.5.6`;这些旧版未包含本次修复。
17
17
 
18
18
  ```bash
19
19
  npm install --global @deepseek-ai/dsh@0.1.7-rc.2
20
- dsh plugin --profile web add dsh-memoir@0.8.1
20
+ dsh plugin --profile web add dsh-memoir@0.8.2
21
21
  ```
22
22
 
23
23
  重启 `dsh web` 即可。记忆保存在本机,不会随插件升级或卸载自动删除。
@@ -87,13 +87,17 @@ memoir_record / memoir_update
87
87
 
88
88
  ## 自动蒸馏
89
89
 
90
- 0.8.1 诊断页区分提醒提交、实际保存、失败、取消、相似记忆待确认和回执降级。只有成功保存的 `memoir_record` / `memoir_update` 才抑制本回合提醒;失败或相似候选待确认不算写入。保存后宿主仍可能取消最终工具结果,因此计数可以重叠。提醒与后续保存的关联不证明因果或内容正确。GUI 手工写入不计入 Agent 工具计数。
90
+ 0.8.2 诊断页区分提醒提交、实际保存、失败、取消、相似记忆待确认和回执降级。只有成功保存的 `memoir_record` / `memoir_update` 才抑制本回合提醒;失败或相似候选待确认不算写入。保存后宿主仍可能取消最终工具结果,因此计数可以重叠。提醒与后续保存的关联不证明因果或内容正确。GUI 手工写入不计入 Agent 工具计数。
91
91
 
92
- 0.8.1 使用公开 `sessionProjections` 从日志和 checkpoint 重建当前回合,不再读取弃用的 `snapshotEvents()`。每会话只保留当前回合最多 4096 个调用 ID,不保存参数或正文;超出保留范围时来源降级为 session-only。门控最多保留 1024 个 Agent,销毁时清理;旧版写入没有保存回执,不能仅从历史调用推断成功。BM25 是词项召回,不承诺跨语言语义匹配。
92
+ 0.8.2 使用公开 `sessionProjections` 从日志和 checkpoint 重建当前回合,不再读取弃用的 `snapshotEvents()`。每会话只保留当前回合最多 4096 个调用 ID,不保存参数或正文;超出保留范围时来源降级为 session-only。门控最多保留 1024 个 Agent,销毁时清理;旧版写入没有保存回执,不能仅从历史调用推断成功。BM25 是词项召回,不承诺跨语言语义匹配。
93
93
 
94
94
  自动蒸馏是可观察的 Agent 收尾提醒,不是后台静默抓取聊天内容。默认 `1 / 0 / 1` 表示:每个有效 worked turn、无额外冷却、至少一次工具调用即可提醒。
95
95
 
96
- `autoDistillEvery`、`autoDistillCooldownMin`、`autoDistillMinTools` 三个条件按 AND 判定并按 Agent 隔离。idle、aborted、subagent 和已调用 `memoir_record` 的回合不会触发;冷却只在提醒成功后更新。所有频率参数都可在 GUI 中即时修改。
96
+ > **0.8.2 修复了新回合的蒸馏答案折叠问题([#13](https://github.com/Qinling-Melon-Farmers/dsh-memoir/issues/13))**:通过公开 `followup` 排入独立记忆收尾回合,不再用同回合 `steer` 抢占原任务回合的最后一步。短回执、空输出或收尾工具失败都不会改变原任务答复的回合边界;记忆来源仍指向原工作回合。收尾回合不计入 worked-turn 频率,也不会递归触发蒸馏。它仍是可见、可取消的模型收尾,不是无成本后台任务,可能增加会话显示的回合数。
97
+ >
98
+ > 升级不会改写旧会话:0.8.0 / 0.8.1 产生的同回合折叠历史仍需展开“用时 / 过程”或切到标准(`normal`)模式查看。本修复不改变 DSH 对任意其他同回合追加步骤的答案选择,也不靠“禁止输出”的提示词掩盖问题。
99
+
100
+ `autoDistillEvery`、`autoDistillCooldownMin`、`autoDistillMinTools` 三个条件按 AND 判定并按 Agent 隔离。idle、aborted、subagent 和已成功保存记忆的回合不会触发;冷却只在提醒成功后更新。所有频率参数都可在 GUI 中即时修改。
97
101
 
98
102
  `language` 独立控制 Agent 可见的工具描述、参数说明、蒸馏提示、工具结果、Hot Memory / `PROJECT_MEMORY.md` 标题以及校验与治理错误。默认 `zh` 保持向后兼容,也可在 GUI 中切换为 `en`;切换后工具 schema 与后续提示即时更新,不要求重启 DSH。
99
103
 
@@ -151,11 +155,11 @@ memoir_record / memoir_update
151
155
 
152
156
  | 渠道 | DSH 基线 | 安装方式 | 状态 |
153
157
  | --- | --- | --- | --- |
154
- | npm `latest`(`0.8.1`) | `>=0.1.7-rc.1 <0.1.8-0` | `dsh plugin --profile web add dsh-memoir@0.8.1` | 0.1.7 兼容线 |
158
+ | npm `latest`(`0.8.2`) | `>=0.1.7-rc.1 <0.1.8-0` | `dsh plugin --profile web add dsh-memoir@0.8.2` | 0.1.7 兼容线 |
155
159
  | npm 固定版 `0.7.1` | `>=0.1.5-rc.1 <0.1.6-0` | `dsh plugin --profile web add dsh-memoir@0.7.1` | 旧 0.1.5 维护线 |
156
160
  | npm 固定版 `0.6.2` | `>=0.1.2-alpha.2 <0.1.3` | `dsh plugin --profile web add dsh-memoir@0.6.2` | 旧 0.1.2 兼容线 |
157
161
  | npm 固定版 `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 兼容线 |
158
- | 源码 `v0.8.1` | `>=0.1.7-rc.1 <0.1.8-0` | 本地构建 + `link:` | 开发调试,不兼容旧 0.1.5 / 0.1.6 |
162
+ | 源码 `v0.8.2` | `>=0.1.7-rc.1 <0.1.8-0` | 本地构建 + `link:` | 开发调试,不兼容旧 0.1.5 / 0.1.6 |
159
163
 
160
164
  需要 Node.js `^22.19.0 || >=24.0.0`。0.7.1 继续使用原生 `conversation.view` / `settings.section` 与 `snapshotEvents()`。DSH 0.1.5 的会话日志升级至 V3;其迁移与 Memoir 的 store v4 / settings v3 是独立格式。升级 DSH 前备份 DSH_HOME,迁移后的 DSH 会话不能承诺被旧宿主读取。Memoir 本次不迁移或清空记忆,也不启用新动态提示词行为;既有会话快照语义保持不变。
161
165
 
@@ -175,7 +179,14 @@ dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
175
179
 
176
180
  </details>
177
181
 
178
- 0.8.1 使用原生 `uiWorkspace`、`conversation.view` / `settings.section` 与 Session V4 专属蒸馏来源。来源链接打开会话,回合编号可复制;不通过全局 DOM 自动滚到回合。设置页继承宿主背景、卡片使用主题层级色,保留皮肤覆盖与独立滚动。store v4 / settings v3 / snapshot v1 不变。升级 DSH 前备份 DSH_HOME,其会话迁移与插件记忆是两回事。
182
+ 0.8.2 使用原生 `uiWorkspace`、`conversation.view` / `settings.section` 与 Session V4 专属蒸馏来源。来源链接打开会话,回合编号可复制;不通过全局 DOM 自动滚到回合。设置页继承宿主背景、卡片使用主题层级色,保留皮肤覆盖与独立滚动。store v4 / settings v3 / snapshot v1 不变。升级 DSH 前备份 DSH_HOME,其会话迁移与插件记忆是两回事。
183
+
184
+ ## 原生侧栏与帮助入口
185
+
186
+ - 会话“记忆”和设置页入口保持不变;右侧栏引导页新增“记忆”,可边对话边看项目记忆、Hot Memory 和诊断,不会自动打开或抢占其它面板。
187
+ - 侧栏读取所属会话的工作区,复用同一数据层;每个实例独立保存当前功能区和滚动状态。缺少侧栏服务时,会话页和设置页仍可用。
188
+ - 任一记忆面板的“记忆设置”底部提供默认折叠的“关于与帮助”:显示插件版本、宿主范围、SDK 基线、维护者,以及仓库、双语文档、Release、反馈链接。插件仓库与当前工作区明确区分。
189
+ - 关于区不后台联网、不探测工作区 Git remote、不上传路径或记忆内容。更新通过宿主插件管理器操作,先核对目标包要求的 DSH 版本和预发布通道;本面板不自动升级。
179
190
 
180
191
  ## 存储、隐私与安全边界
181
192
 
@@ -257,6 +268,6 @@ pnpm test
257
268
  npm run bench
258
269
  ```
259
270
 
260
- 提交前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。版本变化见 [CHANGELOG.md](./CHANGELOG.md),正式包由 tag 工作流通过 npm OIDC 发布。当前版本是 [v0.8.1](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.8.1),面向 DSH 0.1.7;旧 0.1.5 保留 0.7.1。
271
+ 提交前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。版本变化见 [CHANGELOG.md](./CHANGELOG.md),正式包由 tag 工作流通过 npm OIDC 发布。当前版本是 [v0.8.2](https://github.com/Qinling-Melon-Farmers/dsh-memoir/releases/tag/v0.8.2),面向 DSH 0.1.7;旧 0.1.5 保留 0.7.1。
261
272
 
262
273
  Apache-2.0
package/lib/activity.d.ts CHANGED
@@ -9,6 +9,8 @@ declare const schema: z.ZodObject<{
9
9
  calls: z.ZodArray<z.ZodString>;
10
10
  recorded: z.ZodBoolean;
11
11
  reminded: z.ZodBoolean;
12
+ distilling: z.ZodBoolean;
13
+ originTurn: z.ZodNullable<z.ZodNumber>;
12
14
  }, z.core.$strip>;
13
15
  export type MemoirActivity = z.infer<typeof schema>;
14
16
  export declare const emptyActivity: () => MemoirActivity;
package/lib/activity.js CHANGED
@@ -8,11 +8,13 @@ const schema = z.object({
8
8
  calls: z.array(z.string()).max(CALL_LIMIT),
9
9
  recorded: z.boolean(),
10
10
  reminded: z.boolean(),
11
+ distilling: z.boolean(),
12
+ originTurn: z.number().int().positive().nullable(),
11
13
  });
12
- export const emptyActivity = () => ({ turn: 0, toolCalls: 0, calls: [], recorded: false, reminded: false });
14
+ export const emptyActivity = () => ({ turn: 0, toolCalls: 0, calls: [], recorded: false, reminded: false, distilling: false, originTurn: null });
13
15
  export const activityProjection = {
14
16
  key: ACTIVITY_KEY,
15
- stateVersion: 1,
17
+ stateVersion: 2,
16
18
  stateSchema: schema,
17
19
  init: emptyActivity,
18
20
  apply(previous, event) {
@@ -29,10 +31,19 @@ export const activityProjection = {
29
31
  return state;
30
32
  return { ...state, toolCalls: state.toolCalls + 1, calls: [...state.calls, event.data.callId].slice(-CALL_LIMIT) };
31
33
  }
32
- if (event.type === 'agent/inbox/spliced' && event.data.target === 'next-step' &&
34
+ // next-step is retained for replay of pre-0.8.2 sessions. New reminders
35
+ // use next-turn, protecting the original turn's compact answer boundary.
36
+ if (event.type === 'agent/inbox/spliced' &&
33
37
  event.data.inserted.some(message => message.source.kind === 'dsh-memoir')) {
34
38
  return previous.reminded ? previous : { ...previous, reminded: true };
35
39
  }
40
+ // Persisted source identity survives reloads/remounts. Do not rely on a
41
+ // process-local flag: a failed or empty distillation must not recurse.
42
+ if (event.type === 'user/message' && event.data.source.kind === 'dsh-memoir') {
43
+ const origin = event.data.source.originTurn;
44
+ const originTurn = typeof origin === 'number' && Number.isInteger(origin) && origin > 0 && origin < previous.turn ? origin : null;
45
+ return { ...previous, reminded: true, distilling: true, originTurn };
46
+ }
36
47
  return previous;
37
48
  },
38
49
  };
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Automatic turn-end distillation: when the plugin is enabled, each turn of a
3
3
  * top-level agent that did real work (made tool calls) and did not already
4
- * persist memory is followed by one steering step asking the agent to distill
4
+ * persist memory is followed by a separate turn asking the agent to distill
5
5
  * the turn into memoir_record entries. Turns without tool activity are left
6
- * alone (no extra model cost), subagent sessions are never steered, and each
7
- * turn is steered at most once — the steering step runs inside the same turn,
8
- * so the per-turn gate is what lets the turn close afterwards.
6
+ * alone (no extra model cost), subagent sessions are excluded, and each work
7
+ * turn queues at most one reminder. Never steer inside the completed work
8
+ * turn: compact chat selects its last step as the answer. The replayable
9
+ * activity projection excludes reminder turns, including failed/no-op ones.
9
10
  *
10
11
  * Pure decision helpers are exported for unit tests.
11
12
  */
@@ -16,14 +17,15 @@ declare module '@deepseek-ai/dsh-llm/message' {
16
17
  interface MessageSourceMap {
17
18
  'dsh-memoir': {
18
19
  kind: 'dsh-memoir';
20
+ originTurn?: number;
19
21
  };
20
22
  }
21
23
  }
22
- /** The steering prompt injected at the end of an active turn. */
23
- export declare function distillPrompt(language?: MemoirLanguage): string;
24
+ /** The follow-up instruction; the originating work turn is not rewritten. */
25
+ export declare function distillPrompt(language?: MemoirLanguage, originTurn?: number): string;
24
26
  /** Backwards-compatible Chinese prompt constant. */
25
27
  export declare const DISTILL_PROMPT: string;
26
- /** Plugin identity stamped on the steering message source. */
28
+ /** Plugin identity stamped on the follow-up message source. */
27
29
  export declare const AUTO_DISTILL_PLUGIN = "dsh-memoir";
28
30
  /** A minimal event view for the turn-activity scan (data is narrowed inside). */
29
31
  export interface TurnEventLike {
@@ -48,7 +50,7 @@ export interface AutoDistillAgentLike {
48
50
  readonly events?: readonly TurnEventLike[];
49
51
  snapshotEvents?: () => readonly TurnEventLike[];
50
52
  };
51
- steer(message: UserMessage): void;
53
+ followup(message: UserMessage): void;
52
54
  }
53
55
  /** Subagent sessions (and any nested delegation) never get distilled. */
54
56
  export declare function isSubagentSession(agent: AutoDistillAgentLike): boolean;
@@ -69,12 +71,12 @@ export declare class AutoDistillGate {
69
71
  * are ready. Duplicate events never advance the worked-turn counter.
70
72
  */
71
73
  consume(agentId: string, turn: number, toolCalls: number, policy: AutoDistillPolicy, now: number): boolean;
72
- /** Record a successful steer; failed steer attempts do not start cooldown. */
73
- recordSteer(agentId: string, now: number): void;
74
+ /** Record a successful followup; failed followup attempts do not start cooldown. */
75
+ recordReminder(agentId: string, now: number): void;
74
76
  /** Drop all state for one agent (disposal hygiene). */
75
77
  forget(agentId: string): void;
76
78
  }
77
- export type DistillOutcome = 'disabled' | 'subagent' | 'aborted' | 'idle' | 'recorded' | 'duplicate' | 'interval' | 'tools' | 'cooldown' | 'steered' | 'failed' | 'unavailable';
79
+ export type DistillOutcome = 'disabled' | 'subagent' | 'aborted' | 'idle' | 'recorded' | 'duplicate' | 'interval' | 'tools' | 'cooldown' | 'queued' | 'distillation' | 'failed' | 'unavailable';
78
80
  /** Process-local counters only; never retains message content or credentials. */
79
81
  export declare class DistillDiagnostics {
80
82
  private counts;
@@ -95,7 +97,8 @@ export declare class DistillDiagnostics {
95
97
  disabled?: number | undefined;
96
98
  aborted?: number | undefined;
97
99
  idle?: number | undefined;
98
- steered?: number | undefined;
100
+ queued?: number | undefined;
101
+ distillation?: number | undefined;
99
102
  failed?: number | undefined;
100
103
  unavailable?: number | undefined;
101
104
  };
@@ -144,7 +147,7 @@ export declare function installAutoDistill(wire: AutoDistillWire, options: {
144
147
  cooldownMin?: number;
145
148
  minTools?: number;
146
149
  };
147
- /** Optional live language source for the steering instruction. */
150
+ /** Optional live language source for the follow-up instruction. */
148
151
  language?: () => MemoirLanguage;
149
152
  now?: () => number;
150
153
  diagnostics?: DistillDiagnostics;
@@ -1,23 +1,27 @@
1
1
  /**
2
2
  * Automatic turn-end distillation: when the plugin is enabled, each turn of a
3
3
  * top-level agent that did real work (made tool calls) and did not already
4
- * persist memory is followed by one steering step asking the agent to distill
4
+ * persist memory is followed by a separate turn asking the agent to distill
5
5
  * the turn into memoir_record entries. Turns without tool activity are left
6
- * alone (no extra model cost), subagent sessions are never steered, and each
7
- * turn is steered at most once — the steering step runs inside the same turn,
8
- * so the per-turn gate is what lets the turn close afterwards.
6
+ * alone (no extra model cost), subagent sessions are excluded, and each work
7
+ * turn queues at most one reminder. Never steer inside the completed work
8
+ * turn: compact chat selects its last step as the answer. The replayable
9
+ * activity projection excludes reminder turns, including failed/no-op ones.
9
10
  *
10
11
  * Pure decision helpers are exported for unit tests.
11
12
  */
12
13
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
13
14
  import { DEFAULT_MEMOIR_LANGUAGE, hostCopy } from './i18n.js';
14
- /** The steering prompt injected at the end of an active turn. */
15
- export function distillPrompt(language = DEFAULT_MEMOIR_LANGUAGE) {
16
- return hostCopy(language).distillPrompt;
15
+ /** The follow-up instruction; the originating work turn is not rewritten. */
16
+ export function distillPrompt(language = DEFAULT_MEMOIR_LANGUAGE, originTurn) {
17
+ const origin = originTurn === undefined ? '' : language === 'en'
18
+ ? `Source work turn: ${originTurn}. This is a separate memory-only follow-up, not a new user task.\n`
19
+ : `来源工作回合:${originTurn}。这是独立的记忆收尾回合,不是新的用户任务。\n`;
20
+ return origin + hostCopy(language).distillPrompt;
17
21
  }
18
22
  /** Backwards-compatible Chinese prompt constant. */
19
23
  export const DISTILL_PROMPT = distillPrompt();
20
- /** Plugin identity stamped on the steering message source. */
24
+ /** Plugin identity stamped on the follow-up message source. */
21
25
  export const AUTO_DISTILL_PLUGIN = 'dsh-memoir';
22
26
  /** Pure event-fixture fold. Runtime activity comes from the host projection. */
23
27
  export function turnActivity(events, turn) {
@@ -58,7 +62,7 @@ export class AutoDistillGate {
58
62
  consume(agentId, turn, toolCalls, policy, now) {
59
63
  let state = this.states.get(agentId);
60
64
  if (state === undefined) {
61
- state = { lastTurn: -Infinity, workedSinceSteer: 0 };
65
+ state = { lastTurn: -Infinity, workedSinceReminder: 0 };
62
66
  this.states.set(agentId, state);
63
67
  if (this.states.size > this.capacity)
64
68
  this.states.delete(this.states.keys().next().value);
@@ -70,20 +74,20 @@ export class AutoDistillGate {
70
74
  state.lastTurn = turn;
71
75
  this.states.delete(agentId);
72
76
  this.states.set(agentId, state);
73
- state.workedSinceSteer += 1;
74
- const intervalReady = state.workedSinceSteer >= policy.every;
77
+ state.workedSinceReminder += 1;
78
+ const intervalReady = state.workedSinceReminder >= policy.every;
75
79
  const activityReady = toolCalls >= policy.minTools;
76
- const cooldownReady = state.lastSteeredAt === undefined || now - state.lastSteeredAt >= policy.cooldownMs;
80
+ const cooldownReady = state.lastRemindedAt === undefined || now - state.lastRemindedAt >= policy.cooldownMs;
77
81
  this.reason = !intervalReady ? 'interval' : !activityReady ? 'tools' : !cooldownReady ? 'cooldown' : 'ready';
78
82
  return this.reason === 'ready';
79
83
  }
80
- /** Record a successful steer; failed steer attempts do not start cooldown. */
81
- recordSteer(agentId, now) {
84
+ /** Record a successful followup; failed followup attempts do not start cooldown. */
85
+ recordReminder(agentId, now) {
82
86
  const state = this.states.get(agentId);
83
87
  if (state === undefined)
84
88
  return;
85
- state.workedSinceSteer = 0;
86
- state.lastSteeredAt = now;
89
+ state.workedSinceReminder = 0;
90
+ state.lastRemindedAt = now;
87
91
  }
88
92
  /** Drop all state for one agent (disposal hygiene). */
89
93
  forget(agentId) {
@@ -100,7 +104,7 @@ export class DistillDiagnostics {
100
104
  write(outcome) { this.writes[outcome] += 1; }
101
105
  record(outcome, at, turn, toolCalls, agents) {
102
106
  this.counts[outcome] = (this.counts[outcome] ?? 0) + 1;
103
- if (['interval', 'tools', 'cooldown', 'steered', 'failed'].includes(outcome))
107
+ if (['interval', 'tools', 'cooldown', 'queued', 'failed'].includes(outcome))
104
108
  this.workedTurns += 1;
105
109
  this.last = { outcome, at, turn, toolCalls };
106
110
  this.agents = agents;
@@ -142,6 +146,10 @@ export function installAutoDistill(wire, options) {
142
146
  report('duplicate');
143
147
  return;
144
148
  }
149
+ if (activity.turn === turn && activity.distilling) {
150
+ report('distillation', activity.toolCalls);
151
+ return;
152
+ }
145
153
  const { recorded, toolCalls, reminded } = activity.turn === turn ? activity : { recorded: false, toolCalls: 0, reminded: false };
146
154
  if (toolCalls === 0 || recorded) {
147
155
  report(recorded ? 'recorded' : 'idle', toolCalls);
@@ -162,17 +170,17 @@ export function installAutoDistill(wire, options) {
162
170
  return;
163
171
  }
164
172
  try {
165
- agent.steer(createUserMessage({
166
- content: [{ type: 'text', text: distillPrompt(options.language?.()) }],
167
- source: { kind: AUTO_DISTILL_PLUGIN },
173
+ agent.followup(createUserMessage({
174
+ content: [{ type: 'text', text: distillPrompt(options.language?.(), turn) }],
175
+ source: { kind: AUTO_DISTILL_PLUGIN, originTurn: turn },
168
176
  }));
169
177
  }
170
178
  catch (error) {
171
179
  report('failed', toolCalls);
172
180
  throw error;
173
181
  }
174
- gate.recordSteer(agent.id, now);
175
- report('steered', toolCalls);
182
+ gate.recordReminder(agent.id, now);
183
+ report('queued', toolCalls);
176
184
  });
177
185
  const disposeAgent = wire.onDisposed?.((agentId) => { gate.forget(agentId); options.diagnostics?.setAgents(gate.size); });
178
186
  return () => { dispose(); disposeAgent?.(); gate.clear(); options.diagnostics?.setAgents(0); };