dsh-memoir 0.6.0 → 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 +25 -12
- package/README.md +25 -12
- package/lib/autodistill.d.ts +51 -1
- package/lib/autodistill.js +70 -21
- package/lib/client.js +747 -151
- package/lib/client.js.map +3 -3
- package/lib/i18n.js +2 -0
- package/lib/index.js +5 -1
- package/lib/routes.js +4 -0
- package/lib/tools.js +3 -2
- package/package.json +15 -15
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
|
-
> `dsh-memoir@0.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@
|
|
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
|
|
|
@@ -35,7 +35,7 @@ Restart `dsh web`. Memory remains local and is not automatically deleted when th
|
|
|
35
35
|
|
|
36
36
|
It fits personal and local development workflows where a new agent should continue understanding a project. It is not a raw chat backup, multi-user cloud sync service, or vector-semantic knowledge base.
|
|
37
37
|
|
|
38
|
-

|
|
39
39
|
|
|
40
40
|
## How it works
|
|
41
41
|
|
|
@@ -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.
|
|
@@ -98,17 +102,26 @@ Top-5 recall on the fixed quality set is 100%; the repository gate requires at l
|
|
|
98
102
|
|
|
99
103
|
Installing into a DSH-alpha `web` profile registers a native Memory Conversation view and Memory Settings section through official slots. The DSH shell owns layout, navigation, and unload lifecycle; Memoir no longer takes over the legacy sidebar through DOM selectors.
|
|
100
104
|
|
|
101
|
-
- Project memory and all-project global memory;
|
|
105
|
+
- Project memory and all-project global memory, with project groups collapsed by default and complete lifecycle totals;
|
|
102
106
|
- status, section, and keyword filters with BM25 scores;
|
|
103
107
|
- add, edit, pin, archive, restore, and supersede;
|
|
104
108
|
- copy and best-effort navigation for session/turn provenance;
|
|
105
109
|
- Hot Memory Inspector: what the next session will inherit;
|
|
106
110
|
- Retrieval Diagnostics: index, query cache, last query, and session snapshot;
|
|
107
|
-
-
|
|
111
|
+
- permanent `Browse / Settings / Hot Memory / Diagnostics` navigation with an independent bounded scroll position per surface;
|
|
112
|
+
- progressive batches of 20 entries or projects, plus a controlled six-line preview for long memory bodies;
|
|
113
|
+
- the native DSH composer-overlay contract, keeping the final memory visible above the conversation composer while the list scrolls fully;
|
|
114
|
+
- arrow-key, Home, and End navigation for surface tabs, plus `aria-expanded` project disclosures and visible focus states;
|
|
108
115
|
- live GUI Chinese/English switching from `<html lang>`, with a separate `language` setting for agent-facing copy.
|
|
109
116
|
|
|
110
117
|
<details>
|
|
111
|
-
<summary>More
|
|
118
|
+
<summary>More GUI screenshots</summary>
|
|
119
|
+
|
|
120
|
+

|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+

|
|
112
125
|
|
|
113
126
|

|
|
114
127
|
|
|
@@ -124,11 +137,11 @@ Installing into a DSH-alpha `web` profile registers a native Memory Conversation
|
|
|
124
137
|
|
|
125
138
|
| Channel | DSH baseline | Installation | Status |
|
|
126
139
|
| --- | --- | --- | --- |
|
|
127
|
-
| npm `latest` (`0.6.
|
|
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 |
|
|
128
141
|
| pinned npm `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 compatibility line |
|
|
129
|
-
| GitHub `main` | `>=0.1.2-alpha.2 <0.1.3` | source clone + `link:` |
|
|
142
|
+
| GitHub `main` (`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | source clone + `link:` | Development and debugging |
|
|
130
143
|
|
|
131
|
-
Node.js `^22.19.0 || >=24.0.0` is required. v0.6.
|
|
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.
|
|
132
145
|
|
|
133
146
|
<details>
|
|
134
147
|
<summary>Install from source</summary>
|
|
@@ -140,7 +153,7 @@ git clone https://github.com/Qinling-Melon-Farmers/dsh-memoir.git
|
|
|
140
153
|
cd dsh-memoir
|
|
141
154
|
pnpm install --frozen-lockfile
|
|
142
155
|
pnpm run build
|
|
143
|
-
npm install --global @deepseek-ai/dsh@
|
|
156
|
+
npm install --global @deepseek-ai/dsh@0.1.2-rc.1
|
|
144
157
|
dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
|
|
145
158
|
```
|
|
146
159
|
|
|
@@ -197,7 +210,7 @@ v0.5.6 benchmark (Node 24.19, 900/1200-token budget; full data in [`bench/report
|
|
|
197
210
|
|
|
198
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.
|
|
199
212
|
|
|
200
|
-
v0.6.
|
|
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.
|
|
201
214
|
|
|
202
215
|
## FAQ
|
|
203
216
|
|
|
@@ -226,6 +239,6 @@ pnpm test
|
|
|
226
239
|
npm run bench
|
|
227
240
|
```
|
|
228
241
|
|
|
229
|
-
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 release is [v0.6.
|
|
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.
|
|
230
243
|
|
|
231
244
|
Apache-2.0
|
package/README.md
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
无需 embedding、向量数据库或云端记忆服务;npm 包零捆绑运行时依赖,DSH peer 由宿主提供。
|
|
12
12
|
|
|
13
13
|
> [!IMPORTANT]
|
|
14
|
-
> `dsh-memoir@0.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@
|
|
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
|
|
|
@@ -34,7 +34,7 @@ dsh plugin --profile web add dsh-memoir@latest
|
|
|
34
34
|
|
|
35
35
|
适合需要“新 Agent 接手时继续理解项目”的个人或本地开发工作流。它不是原始聊天记录备份、多人云同步服务或向量语义知识库。
|
|
36
36
|
|
|
37
|
-

|
|
38
38
|
|
|
39
39
|
## 工作原理
|
|
40
40
|
|
|
@@ -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 中即时修改。
|
|
@@ -97,17 +101,26 @@ memoir_record / memoir_update
|
|
|
97
101
|
|
|
98
102
|
安装到 DSH alpha 的 `web` profile 后,Memoir 通过官方 slot 注册原生「记忆」会话视图和「记忆」Settings 分区;布局、导航与卸载生命周期均由 DSH shell 管理,不再通过 DOM 选择器接管旧侧边栏。
|
|
99
103
|
|
|
100
|
-
-
|
|
104
|
+
- 项目记忆与所有项目的全局记忆;全局视图按项目默认折叠并显示完整生命周期计数;
|
|
101
105
|
- 状态、分类和关键词筛选,BM25 分数展示;
|
|
102
106
|
- 新增、编辑、置顶、归档、恢复和替代;
|
|
103
107
|
- session/turn 来源复制与尽力跳转;
|
|
104
108
|
- Hot Memory Inspector:下一会话将继承什么;
|
|
105
109
|
- Retrieval Diagnostics:索引、查询缓存、最近查询和会话快照;
|
|
106
|
-
-
|
|
110
|
+
- 常驻 `记忆浏览 / 记忆设置 / Hot Memory / 诊断` 二级导航,各功能区拥有独立有界滚动位置;
|
|
111
|
+
- 每批渐进展示 20 条记忆或 20 个项目,长正文默认折叠为六行并可显式展开;
|
|
112
|
+
- 使用 DSH 原生 composer-overlay 契约,长列表可完整滚动且最后一项不会被对话输入框遮挡;
|
|
113
|
+
- 页签支持方向键、Home、End,项目折叠具备 `aria-expanded` 与清晰焦点状态;
|
|
107
114
|
- GUI 跟随 `<html lang>` 在中文和英文间即时切换;Agent 侧语言由独立的 `language` 设置控制。
|
|
108
115
|
|
|
109
116
|
<details>
|
|
110
|
-
<summary
|
|
117
|
+
<summary>查看更多 GUI 截图</summary>
|
|
118
|
+
|
|
119
|
+

|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+

|
|
111
124
|
|
|
112
125
|

|
|
113
126
|
|
|
@@ -123,11 +136,11 @@ memoir_record / memoir_update
|
|
|
123
136
|
|
|
124
137
|
| 渠道 | DSH 基线 | 安装方式 | 状态 |
|
|
125
138
|
| --- | --- | --- | --- |
|
|
126
|
-
| npm `latest`(`0.6.
|
|
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 验证 |
|
|
127
140
|
| npm 固定版 `0.5.6` | `0.1.1-rc.2` | `dsh plugin --profile web add dsh-memoir@0.5.6` | rc2 兼容线 |
|
|
128
|
-
| GitHub `main
|
|
141
|
+
| GitHub `main`(`0.6.2`) | `>=0.1.2-alpha.2 <0.1.3` | 源码 clone + `link:` | 开发和调试使用 |
|
|
129
142
|
|
|
130
|
-
需要 Node.js `^22.19.0 || >=24.0.0`。`0.6.
|
|
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` 会拒绝不兼容宿主。
|
|
131
144
|
|
|
132
145
|
<details>
|
|
133
146
|
<summary>从源码安装</summary>
|
|
@@ -139,7 +152,7 @@ git clone https://github.com/Qinling-Melon-Farmers/dsh-memoir.git
|
|
|
139
152
|
cd dsh-memoir
|
|
140
153
|
pnpm install --frozen-lockfile
|
|
141
154
|
pnpm run build
|
|
142
|
-
npm install --global @deepseek-ai/dsh@
|
|
155
|
+
npm install --global @deepseek-ai/dsh@0.1.2-rc.1
|
|
143
156
|
dsh plugin --profile web add "link:/absolute/path/dsh-memoir"
|
|
144
157
|
```
|
|
145
158
|
|
|
@@ -196,7 +209,7 @@ v0.5.6 基准(Node 24.19,900/1200 token;完整数据见 [`bench/report.md`
|
|
|
196
209
|
|
|
197
210
|
基准值取决于机器和语料;它证明的重点是注入预算保持有界、缓存命中路径与记忆总量解耦。
|
|
198
211
|
|
|
199
|
-
v0.6.
|
|
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 更新日志。
|
|
200
213
|
|
|
201
214
|
## 常见问题
|
|
202
215
|
|
|
@@ -225,6 +238,6 @@ pnpm test
|
|
|
225
238
|
npm run bench
|
|
226
239
|
```
|
|
227
240
|
|
|
228
|
-
提交前请阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。版本变化见 [CHANGELOG.md](./CHANGELOG.md),正式包由 tag 工作流通过 npm OIDC
|
|
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` 与该版本同步。
|
|
229
242
|
|
|
230
243
|
Apache-2.0
|
package/lib/autodistill.d.ts
CHANGED
|
@@ -27,6 +27,16 @@ export interface TurnActivity {
|
|
|
27
27
|
recorded: boolean;
|
|
28
28
|
toolCalls: number;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Session-log compatibility surface. DSH <= alpha.3 exposed `events` while
|
|
32
|
+
* alpha.4+ keeps the log private and exposes an immutable snapshot method.
|
|
33
|
+
*/
|
|
34
|
+
export interface SessionEventSource {
|
|
35
|
+
readonly events?: readonly TurnEventLike[];
|
|
36
|
+
snapshotEvents?: () => readonly TurnEventLike[];
|
|
37
|
+
}
|
|
38
|
+
/** Read a stable session event snapshot across the old and new DSH APIs. */
|
|
39
|
+
export declare function sessionEventSnapshot(session: SessionEventSource | undefined): readonly TurnEventLike[];
|
|
30
40
|
/** Scan the tail of a session log for one turn's tool activity. */
|
|
31
41
|
export declare function turnActivity(events: readonly TurnEventLike[], turn: number): TurnActivity;
|
|
32
42
|
/** The agent surface the turn-stopping listener needs. */
|
|
@@ -37,7 +47,8 @@ export interface AutoDistillAgentLike {
|
|
|
37
47
|
origin?: string;
|
|
38
48
|
delegationDepth?: number;
|
|
39
49
|
};
|
|
40
|
-
events
|
|
50
|
+
readonly events?: readonly TurnEventLike[];
|
|
51
|
+
snapshotEvents?: () => readonly TurnEventLike[];
|
|
41
52
|
};
|
|
42
53
|
steer(message: UserMessage): void;
|
|
43
54
|
}
|
|
@@ -51,6 +62,10 @@ export interface AutoDistillPolicy {
|
|
|
51
62
|
/** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
|
|
52
63
|
export declare class AutoDistillGate {
|
|
53
64
|
private states;
|
|
65
|
+
readonly capacity = 1024;
|
|
66
|
+
reason: 'duplicate' | 'interval' | 'tools' | 'cooldown' | 'ready';
|
|
67
|
+
get size(): number;
|
|
68
|
+
clear(): void;
|
|
54
69
|
/**
|
|
55
70
|
* Consume one eligible worked turn and decide whether all policy conditions
|
|
56
71
|
* are ready. Duplicate events never advance the worked-turn counter.
|
|
@@ -61,6 +76,39 @@ export declare class AutoDistillGate {
|
|
|
61
76
|
/** Drop all state for one agent (disposal hygiene). */
|
|
62
77
|
forget(agentId: string): void;
|
|
63
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
|
+
}
|
|
64
112
|
export interface TurnStoppingPayload {
|
|
65
113
|
agent: AutoDistillAgentLike;
|
|
66
114
|
turn: number;
|
|
@@ -69,6 +117,7 @@ export interface TurnStoppingPayload {
|
|
|
69
117
|
/** The event-wire surface the installer needs (satisfied by ctx.on). */
|
|
70
118
|
export interface AutoDistillWire {
|
|
71
119
|
on(name: 'agent/turn-stopping', listener: (payload: TurnStoppingPayload) => void): () => void;
|
|
120
|
+
onDisposed?(listener: (agentId: string) => void): () => void;
|
|
72
121
|
}
|
|
73
122
|
/**
|
|
74
123
|
* Install the turn-end listener. The returned disposer removes the listener.
|
|
@@ -89,4 +138,5 @@ export declare function installAutoDistill(wire: AutoDistillWire, options: {
|
|
|
89
138
|
/** Optional live language source for the steering instruction. */
|
|
90
139
|
language?: () => MemoirLanguage;
|
|
91
140
|
now?: () => number;
|
|
141
|
+
diagnostics?: DistillDiagnostics;
|
|
92
142
|
}): () => void;
|
package/lib/autodistill.js
CHANGED
|
@@ -19,6 +19,12 @@ export function distillPrompt(language = DEFAULT_MEMOIR_LANGUAGE) {
|
|
|
19
19
|
export const DISTILL_PROMPT = distillPrompt();
|
|
20
20
|
/** Plugin identity stamped on the steering message source. */
|
|
21
21
|
export const AUTO_DISTILL_PLUGIN = 'dsh-memoir';
|
|
22
|
+
/** Read a stable session event snapshot across the old and new DSH APIs. */
|
|
23
|
+
export function sessionEventSnapshot(session) {
|
|
24
|
+
if (typeof session?.snapshotEvents === 'function')
|
|
25
|
+
return session.snapshotEvents();
|
|
26
|
+
return session?.events ?? [];
|
|
27
|
+
}
|
|
22
28
|
/** Scan the tail of a session log for one turn's tool activity. */
|
|
23
29
|
export function turnActivity(events, turn) {
|
|
24
30
|
let recorded = false;
|
|
@@ -34,7 +40,7 @@ export function turnActivity(events, turn) {
|
|
|
34
40
|
continue;
|
|
35
41
|
if (event.type === 'tool/call') {
|
|
36
42
|
toolCalls += 1;
|
|
37
|
-
if (data.name === 'memoir_record')
|
|
43
|
+
if (data.name === 'memoir_record' || data.name === 'memoir_update')
|
|
38
44
|
recorded = true;
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -47,6 +53,10 @@ export function isSubagentSession(agent) {
|
|
|
47
53
|
/** Per-agent frequency, cooldown, and duplicate-turn state (with pruning). */
|
|
48
54
|
export class AutoDistillGate {
|
|
49
55
|
states = new Map();
|
|
56
|
+
capacity = 1024;
|
|
57
|
+
reason = 'ready';
|
|
58
|
+
get size() { return this.states.size; }
|
|
59
|
+
clear() { this.states.clear(); }
|
|
50
60
|
/**
|
|
51
61
|
* Consume one eligible worked turn and decide whether all policy conditions
|
|
52
62
|
* are ready. Duplicate events never advance the worked-turn counter.
|
|
@@ -54,21 +64,24 @@ export class AutoDistillGate {
|
|
|
54
64
|
consume(agentId, turn, toolCalls, policy, now) {
|
|
55
65
|
let state = this.states.get(agentId);
|
|
56
66
|
if (state === undefined) {
|
|
57
|
-
state = {
|
|
67
|
+
state = { lastTurn: -Infinity, workedSinceSteer: 0 };
|
|
58
68
|
this.states.set(agentId, state);
|
|
69
|
+
if (this.states.size > this.capacity)
|
|
70
|
+
this.states.delete(this.states.keys().next().value);
|
|
59
71
|
}
|
|
60
|
-
if (state.
|
|
72
|
+
if (turn <= state.lastTurn) {
|
|
73
|
+
this.reason = 'duplicate';
|
|
61
74
|
return false;
|
|
62
|
-
state.processedTurns.add(turn);
|
|
63
|
-
for (const value of [...state.processedTurns]) {
|
|
64
|
-
if (value < turn - 100)
|
|
65
|
-
state.processedTurns.delete(value);
|
|
66
75
|
}
|
|
76
|
+
state.lastTurn = turn;
|
|
77
|
+
this.states.delete(agentId);
|
|
78
|
+
this.states.set(agentId, state);
|
|
67
79
|
state.workedSinceSteer += 1;
|
|
68
80
|
const intervalReady = state.workedSinceSteer >= policy.every;
|
|
69
81
|
const activityReady = toolCalls >= policy.minTools;
|
|
70
82
|
const cooldownReady = state.lastSteeredAt === undefined || now - state.lastSteeredAt >= policy.cooldownMs;
|
|
71
|
-
|
|
83
|
+
this.reason = !intervalReady ? 'interval' : !activityReady ? 'tools' : !cooldownReady ? 'cooldown' : 'ready';
|
|
84
|
+
return this.reason === 'ready';
|
|
72
85
|
}
|
|
73
86
|
/** Record a successful steer; failed steer attempts do not start cooldown. */
|
|
74
87
|
recordSteer(agentId, now) {
|
|
@@ -83,6 +96,22 @@ export class AutoDistillGate {
|
|
|
83
96
|
this.states.delete(agentId);
|
|
84
97
|
}
|
|
85
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
|
+
}
|
|
86
115
|
/**
|
|
87
116
|
* Install the turn-end listener. The returned disposer removes the listener.
|
|
88
117
|
* @param wire - the event wire (the cordis context).
|
|
@@ -92,30 +121,50 @@ export function installAutoDistill(wire, options) {
|
|
|
92
121
|
const gate = new AutoDistillGate();
|
|
93
122
|
const integerAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, Math.floor(value)) : fallback;
|
|
94
123
|
const numberAtLeast = (value, fallback, minimum) => typeof value === 'number' && Number.isFinite(value) ? Math.max(minimum, value) : fallback;
|
|
95
|
-
|
|
96
|
-
if (!options.enabled())
|
|
97
|
-
return;
|
|
124
|
+
const dispose = wire.on('agent/turn-stopping', (payload) => {
|
|
98
125
|
const { agent, turn, signal } = payload;
|
|
99
|
-
|
|
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');
|
|
100
130
|
return;
|
|
101
|
-
|
|
131
|
+
}
|
|
132
|
+
if (isSubagentSession(agent)) {
|
|
133
|
+
report('subagent');
|
|
102
134
|
return;
|
|
103
|
-
|
|
104
|
-
if (
|
|
135
|
+
}
|
|
136
|
+
if (signal.aborted) {
|
|
137
|
+
report('aborted');
|
|
105
138
|
return;
|
|
139
|
+
}
|
|
140
|
+
const { worked, recorded, toolCalls } = turnActivity(sessionEventSnapshot(agent.session), turn);
|
|
141
|
+
if (!worked || recorded) {
|
|
142
|
+
report(recorded ? 'recorded' : 'idle', toolCalls);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
106
145
|
const live = options.policy?.();
|
|
107
146
|
const policy = {
|
|
108
147
|
every: integerAtLeast(live?.every ?? options.every, 1, 1),
|
|
109
148
|
cooldownMs: numberAtLeast(live?.cooldownMin ?? options.cooldownMin, 0, 0) * 60_000,
|
|
110
149
|
minTools: integerAtLeast(live?.minTools ?? options.minTools, 1, 1),
|
|
111
150
|
};
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
if (!gate.consume(agent.id, turn, toolCalls, policy, now)) {
|
|
152
|
+
report(gate.reason === 'ready' ? 'duplicate' : gate.reason, toolCalls);
|
|
114
153
|
return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
165
|
gate.recordSteer(agent.id, now);
|
|
166
|
+
report('steered', toolCalls);
|
|
120
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); };
|
|
121
170
|
}
|