@ilaomianyang/dsh-memory-graph 0.11.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 — 2026-11-03
4
+
5
+ 装完之后就不用再管它了:插件自己去问有没有新版本,积压的记忆自己一批批追平,视图跟着库走。
6
+
7
+ **Added**
8
+
9
+ - **自我更新**。设置页最上面一行「版本与更新」:当前版本、检查更新、立即更新。启动时查一次,之后每 `update.checkEveryHours` 小时(默认 12)最多一次——一次 `GET <registry>/@ilaomianyang%2fdsh-memory-graph/latest`,不带查询串、不带记忆库路径、不带任何对话内容;结果缓存进库里的 `.update-state.json`,离线时保留上一次的答案,不会把"查不到"显示成"已是最新"。
10
+ - 安装走宿主自己的 `pluginManager.installBundle`(和 `dsh plugin` 同一条 pnpm 路径,尊重已配的 registry),装完照实说「需要重启 Harness 生效」还是「已生效」。**`link:` / `file:` 本地安装一律拒绝**,并说明会覆盖开发目录——这句是拒绝,不是报错。
11
+ - `update.autoInstall`(默认关):启动时发现新版本就自己装。它在任何对话开始之前动手,是唯一"换掉插件不会打断你正在说的话"的时刻;`link:` 安装同样拒绝。`memory_stats` 也报一行版本。
12
+ - 新路由 `/api/memory-graph.update`:GET 读状态(`current/latest/behind/canInstall/installKind/busy/message`),POST 是唯一能装的方法,所以页面加载或预取永远不会误装。
13
+ - **追平模式**(`auto.catchUp`,默认关)。原来的两条规则一次只消化一批,两千条积压要等几十天;打开追平后,只要还攒着**一整批**(≥ `maxEpisodesPerRun`)就按自己的节奏(`catchUpEveryMinutes`,默认 10 分钟)一批批蒸下去,一天最多 `auto.maxBatchesPerDay` 批(默认 6,0 = 不限),追平就自己停。每日批次计数存进 `.auto-state.json` 的 `batchDay`/`batchesToday`,重启不重置;批次在调用**之前**就计数,失败也算用掉一个名额——上限才是真的上限。静默时段压过追平。
14
+ - `memory_backfill` 写完后顺手重画 `README.md` 和 `graph.html`(报告里 `refreshed`),导入两千条情节后打开视图看到的是当下的库,不是导入前的空图。
15
+ - `test/update.mjs` 27 项 + `test/auto.mjs` 加 6 项追平检查。检查数 244 → 277。
16
+
17
+ **Changed**
18
+
19
+ - 设置页多了 7 个旋钮:`auto.catchUp` / `auto.catchUpEveryMinutes` / `auto.maxBatchesPerDay`,以及新的「版本与更新」分区(`update.enabled` / `checkEveryHours` / `autoInstall` / `registry`)。版本那一行不是 config 字段,是这一块自己 GET/POST 出来的 UI,刻意不占 `mg-card` / `mg-row` 这些类名。
20
+ - `README.md` / `README.en.md`:「自动蒸馏」从两条规则写成三条,新增「它自己更新」一节,配置表 34 → 41 行,YAML 例子带上 `auto.catchUp` 与 `update`。
21
+
3
22
  ## 0.11.0 — 2026-11-03
4
23
 
5
24
  历史会话也能进库了,宿主侧的文案统一成中文。
package/README.en.md CHANGED
@@ -96,6 +96,23 @@ After the restart it is **Settings → Memory Graph**, between Models and Plugin
96
96
 
97
97
  Uninstall with `plugin_manager action=remove_bundle target="@ilaomianyang/dsh-memory-graph"`. The vault is not touched — it lives in `~/dsh-memory`, independent of the plugin.
98
98
 
99
+ ## It updates itself
100
+
101
+ The copy installed from npm gets a **Version & updates** row at the top of its Settings page: the version you are running, **Check**, and **Update**.
102
+
103
+ ![Version & updates in Settings: the current version, Check, Update, then four switches](docs/update-settings.png)
104
+
105
+ That is the real page (on a development machine the install is a `link:`, so Update is greyed out and says why next to it): the version, whether it is current, which registry, when it last checked — and the four `update.*` switches below.
106
+
107
+ - It checks once at startup, then at most every `update.checkEveryHours` hours (default 12). The request is one `GET` of `<registry>/@ilaomianyang%2fdsh-memory-graph/latest` — no query string, no vault path, no conversation content. The answer is cached in the vault's `.update-state.json`, so an offline machine keeps the last answer instead of showing "could not check" as "up to date".
108
+ - Nothing is installed until you press Update. Installation goes through the host's own `pluginManager.installBundle` — the same path `dsh plugin` and the plugin manager use — so it respects your configured registry instead of inventing a second package manager.
109
+ - **A `link:` install is refused**, and the refusal is a sentence rather than an error: "updating would overwrite your working copy". For a clone, updating is `git pull` and a reload, as above.
110
+ - An install usually needs a Harness restart (same caching reason as `client.js` / `package.json`). The page says **restart Harness to apply** rather than pretending it is already running the new code.
111
+ - `update.autoInstall` (off by default) installs a newer version at startup without being asked. It acts before any conversation starts, which is the one moment swapping the plugin cannot interrupt something you were saying. A `link:` install is still refused.
112
+ - `memory_stats` reports the version too, so the model does not have to guess which copy it is talking to.
113
+
114
+ Turn `update.enabled` off to stop it going online entirely: after that it sends nothing, and the page only shows the current version.
115
+
99
116
  ## What you get on disk
100
117
 
101
118
  ```
@@ -145,6 +162,13 @@ One file is one memory. Append-only. No binaries, no lockfiles. Run `git init` i
145
162
  retrieval:
146
163
  limit: 8
147
164
  hops: 1
165
+ auto:
166
+ enabled: true
167
+ catchUp: true # drain a backlog, one batch at a time
168
+ maxBatchesPerDay: 6
169
+ update:
170
+ enabled: true # check for a newer version at startup
171
+ autoInstall: false
148
172
  ```
149
173
 
150
174
  | Key | Default | Meaning | On the page |
@@ -183,37 +207,52 @@ One file is one memory. Append-only. No binaries, no lockfiles. Run `git init` i
183
207
  | `auto.minGapMinutes` | `30` | shared by both rules: minimum gap between two automatic runs | ✓ |
184
208
  | `auto.quietHours` | `''` | window in which nothing runs (e.g. `23:00-08:00`, may cross midnight) | ✓ |
185
209
  | `auto.project` | `''` | project automatic cards are filed under | ✓ |
210
+ | `auto.catchUp` | `false` | catch-up: keep running batches while a full batch is still waiting | ✓ |
211
+ | `auto.catchUpEveryMinutes` | `10` | minutes between catch-up batches (its own cadence) | ✓ |
212
+ | `auto.maxBatchesPerDay` | `6` | most automatic batches per day, 0 lifts the cap; the one ceiling on automatic spending | ✓ |
213
+ | `update.enabled` | `true` | ask npm for a newer version regularly (checks only, nothing about the vault leaves) | ✓ |
214
+ | `update.checkEveryHours` | `12` | how long a check result stays good for | ✓ |
215
+ | `update.autoInstall` | `false` | install a newer version at startup (a `link:` install is refused) | ✓ |
216
+ | `update.registry` | `''` | which npm registry to ask; empty = the harness's, then npmjs | ✓ |
186
217
  | `retrieval.limit` | `8` | how many cards one search returns | ✓ |
187
218
  | `retrieval.hops` | `1` | entity expansion depth, `0` = keyword only | ✓ |
188
219
  | `retrieval.snippetChars` | `320` | how much of each hit the model gets to read | ✓ |
189
220
  | `retrieval.timelineLimit` | `20` | how far back `memory_timeline` looks by default | ✓ |
190
221
 
191
- Why only those 34 are editable there: the harness projects **only fields declared `volatile`** — the ones read each time they are used. Everything marked ✓ is read at call time, so one save changes the next call. `enabled` / `vaultDir` / `defaultProject` / `crossProject` are consumed during activation and would need a remount, so they are left out rather than shown as knobs that do nothing; the page's footer says exactly that.
222
+ Why only those 41 are editable there: the harness projects **only fields declared `volatile`** — the ones read each time they are used. Everything marked ✓ is read at call time, so one save changes the next call. `enabled` / `vaultDir` / `defaultProject` / `crossProject` are consumed during activation and would need a remount, so they are left out rather than shown as knobs that do nothing; the page's footer says exactly that.
192
223
 
193
- ### When it distils on its own: two rules
224
+ ### When it distils on its own: three rules
194
225
 
195
226
  By default it does **not**. With `auto.enabled` on, the plugin looks once a minute at whether a run is due — and that look is arithmetic only (how many episodes are waiting, how long since the last attempt, is this a quiet hour). **No model call, no cost.** Only a rule that matches starts a run, and it runs the same `distill()`.
196
227
 
197
- The two rules are independent: either one alone, or both.
228
+ The three rules are independent: any one alone, or all of them.
198
229
 
199
230
  | Rule | Setting | Fires when |
200
231
  | --- | --- | --- |
201
232
  | Distil once enough pile up | `auto.minEpisodes` | undistilled episodes ≥ this number (say 10) |
202
233
  | Distil on a schedule | `auto.everyMinutes` | this many minutes have passed since the last automatic run (1440 = daily) |
234
+ | Catch up | `auto.catchUp` | a **full batch** is still waiting (≥ `maxEpisodesPerRun`): run batch after batch, `auto.catchUpEveryMinutes` apart (default 10), at most `auto.maxBatchesPerDay` batches a day (default 6, 0 lifts the cap) |
235
+
236
+ Catch-up is for a **backlog** — an imported history, a week away from the machine, two thousand undistilled episodes. The other two rules digest one batch per trigger and would take dozens of days; catch-up keeps going until less than a batch is left, then goes quiet.
237
+
238
+ - It has **its own cadence** (`catchUpEveryMinutes`) and is not held back by `minGapMinutes`; the other two rules still are.
239
+ - `maxBatchesPerDay` is the only knob that puts a **ceiling on automatic spending**: the default six batches is about 240 episodes a day. Raise it to drain faster, or set `quietHours` so it does not run at night.
240
+ - An interrupted catch-up (restart, shutdown, another run holding the lock) loses nothing: the counter lives in `.auto-state.json`, the next interval continues, and nothing already distilled is distilled twice.
241
+ - 2080 episodes at the defaults: six batches a day × 40 episodes ≈ nine days. Set the cap to 50 to finish in a day.
203
242
 
204
- Three gates both rules share:
243
+ Three gates the rules share:
205
244
 
206
- - `auto.minGapMinutes` (default 30): never two automatic runs closer than this. It is checked before the rules — a vault that just crossed the threshold should not be asked again a minute later, and every ask costs money.
207
- - `auto.quietHours` (default empty): nothing runs inside this window; it may cross midnight (`23:00-08:00`). Unreadable text is ignored rather than silencing the feature forever.
245
+ - `auto.minGapMinutes` (default 30): the two **non-catch-up** rules never fire closer than this. It is checked before them — a vault that just crossed the threshold should not be asked again a minute later, and every ask costs money.
246
+ - `auto.quietHours` (default empty): nothing runs inside this window; it may cross midnight (`23:00-08:00`). Unreadable text is ignored rather than silencing the feature forever. Catch-up obeys it too.
208
247
  - `auto.project` (default empty): which project automatic cards are filed under; empty uses the newest episode's project.
209
248
 
210
249
  A few deliberate choices:
211
250
 
212
251
  - **The schedule clock starts when you switch it on**: it does not run on the spot — a daily schedule should not spend the moment you flip the switch. That clock is stored separately (`startedAt`) and is not treated as "the last run", so it does not push the threshold rule back by one minimum gap either.
213
- - **The gap is armed before the call**, so a run that produced nothing still counts and will not be re-asked the next minute. It is recorded in `.auto-state.json` in the vault (`lastAttemptAt` only), so **a restart does not reset it**.
252
+ - **The gap is armed before the call**, so a run that produced nothing still counts and will not be re-asked the next minute. The catch-up counter works the same way: it is incremented before the call, so a failed batch still spends its slot and the daily cap stays a real cap. Both live in `.auto-state.json` in the vault (`lastAttemptAt`, `startedAt`, `batchDay`, `batchesToday`), so **a restart does not reset them**.
214
253
  - That file is not `.distill-state.json`: the latter is the **cursor** (which lines have been read), the former is the **attempt clock**. Merging them would make "read, but produced nothing" impossible to express.
215
254
  - Automatic runs, the button and `memory_distill` share **one lock**: while a run is in flight, the other callers are told so rather than queued, so two of them can never spend at once.
216
- - To stop it: `auto.enabled = false` (master switch), or set both rules to 0 (the switch stays on, no rule matches, nothing runs).
255
+ - To stop it: `auto.enabled = false` (master switch), or set the rules to 0 (the switch stays on, nothing matches, nothing runs).
217
256
 
218
257
  ![Automatic distillation in Settings: the switch, the two rules, the minimum gap, quiet hours and the project](docs/auto-settings.png)
219
258
 
@@ -289,7 +328,7 @@ memory_backfill {dryRun: true} → 133 sessions scanned / 2078 episodes woul
289
328
  memory_backfill {dryRun: false} → 2078 episodes written; 85 already present, skipped
290
329
  ```
291
330
 
292
- What lands is **episodes**, not cards: distil once (`memory_distill`, the Settings button, or the daily auto run at 40 episodes a batch) and they enter the retrieval layer. Two thousand episodes is fifty model batches — that is a spend worth deciding on deliberately.
331
+ What lands is **episodes**, not cards: distil once (`memory_distill`, the Settings button, or the daily auto run at 40 episodes a batch) and they enter the retrieval layer. Two thousand episodes is fifty model batches — that is a spend worth deciding on deliberately, and if you would rather have it drain itself, switch on `auto.catchUp`: it runs at most `auto.maxBatchesPerDay` batches a day and stops when it has caught up. A backfill also re-renders `README.md` and `graph.html` when it finishes, so those two files show the vault as it is now rather than as it was before the import.
293
332
 
294
333
  A session file is a **concatenated zstd container** (one frame per appended batch), so reading it means cutting it frame by frame — `src/history.js` carries a scanner equivalent to the one the harness itself uses.
295
334
 
@@ -404,7 +443,7 @@ Side benefit: an injected message no longer cuts a turn in half, so an episode's
404
443
  No network, no real vault:
405
444
 
406
445
  ```bash
407
- npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 19 + 12 = 244 checks, plus the 25-question retrieval eval
446
+ npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 25 + 12 + 27 = 277 checks, plus the 25-question retrieval eval
408
447
  npm run eval # retrieval eval only (offline, deterministic)
409
448
  ```
410
449
 
@@ -416,7 +455,8 @@ npm run eval # retrieval eval only (offline, deterministic)
416
455
  - `test/tools.mjs` — runs `execute` + `render` for all nine tools
417
456
  - `test/page.mjs` — the route behind the sidebar panel: `/api/memory-graph.page` sits on the authenticated channel, an empty vault still answers with a page, it is **the same renderer as the `graph.html` on disk** (only the timestamp differs), `X-Frame-Options: SAMEORIGIN` so only the app can frame it, a broken vault prints the reason on the page (escaped), no vault at all is a 500 rather than a crash, and no vault path ever appears in the bytes
418
457
  - `test/distillapi.mjs` — the Distill now door: `/api/memory-graph.distill` on the authenticated `/api` channel, running the same `distill()` (live vault, the model selected in Settings, the live redaction rules), one press is one run, a concurrent second press is refused instead of queued, an extractor refusal is a 200 and not a 500, a throw is a 500 with the lock still released, views are re-rendered only when cards were written, a render failure only warns, the card list is capped at 40, and a route with no runner answers 500 instead of throwing at the browser
419
- - `test/auto.mjs` — the automatic distiller: each rule fires on its own, whichever matches first runs when both are on, the minimum gap is checked before the rules, a quiet window may cross midnight, a tick only decides (an off switch does not even read the vault), one tick is at most one run, the gap is armed before the call, a restart reads `.auto-state.json` instead of starting over, a run that wrote nothing still arms the gap, an in-flight run is not stacked on, failures are logged with their reason, the project reaches the run, an unreadable vault is a skip and not a crash, and the tick asks for enough episodes to see the threshold
458
+ - `test/auto.mjs` — the automatic distiller: each rule fires on its own, whichever matches first runs when both are on, the minimum gap is checked before the rules, a quiet window may cross midnight, a tick only decides (an off switch does not even read the vault), one tick is at most one run, the gap is armed before the call, a restart reads `.auto-state.json` instead of starting over, a run that wrote nothing still arms the gap, an in-flight run is not stacked on, failures are logged with their reason, the project reaches the run, an unreadable vault is a skip and not a crash, and the tick asks for enough episodes to see the threshold; catch-up gets its own group: less than a full batch is not a backlog, it keeps its own ten-minute cadence instead of the minimum gap, it stops at the daily cap and starts again the next day
459
+ - `test/update.mjs` — self-update: version comparison (a prerelease sorts below its release, a `v` prefix is ignored), a `link:`/`file:` install is recognised as a working copy and therefore refuses to update itself, the registry version and the status line in prose, one registry request inside the window, `force` does not re-ask within a minute, the cache expires on its own, an outage keeps the last answer instead of claiming "up to date", turning checks off sends nothing, a configured registry wins over the host's, an unusable plugin manager does not stop the check, installing asks for the exact version through `installBundle`, applied / restart-required / failed each read differently, a second install while one runs is refused rather than queued, `autoInstall` acts only when allowed and only on a registry install, and the route layer: GET is uncached, POST is the only verb that can install, no checker is a 503
420
460
  - `test/extract.mjs` — the extractor: `fetch` stubbed for the http route (prompt construction, key resolution order, tolerant JSON parsing, per-card project attribution, cursor advance, dry run writing nothing, secrets stopped before the write), then a fake `llm` service for the harness route (borrows the Settings model and thinking level, honours an explicit override, explains a missing selection, handles every terminal `finish`, reports cancellation, rejects an empty completion, and never touches `fetch`), and finally batching: the batch size derived from the output budget, a long backlog split across calls, a truncated batch halved and retried, earlier batches kept when a later one fails, and `dryRun` previewing only the first batch
421
461
 
422
462
  Two suites do touch the network (and cost money), so they are not in `npm test`:
@@ -443,7 +483,7 @@ The tag triggers `.github/workflows/publish.yml`, which does four things:
443
483
 
444
484
  1. checks that the tag and `package.json` agree — a mismatch fails instead of publishing a package whose version lies;
445
485
  2. checks whether that version is already on the registry — if so it skips, so re-running a tag is a no-op rather than an error;
446
- 3. `npm publish --provenance --access public` (`prepublishOnly` runs the 244 checks again; a red build cannot reach the registry);
486
+ 3. `npm publish --provenance --access public` (`prepublishOnly` runs the 277 checks again; a red build cannot reach the registry);
447
487
  4. adds "Built and signed on GitHub Actions" to the npm page, proving the tarball came from this commit of this repository.
448
488
 
449
489
  Two ways to authenticate; the workflow accepts both (it uses `NPM_TOKEN` when the secret exists and falls back to OIDC when it does not):
package/README.md CHANGED
@@ -90,6 +90,23 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
90
90
 
91
91
  重启后打开 **设置 → 记忆图谱**(在「模型」和「内置插件」之间)。卸载:`plugin_manager action=remove_bundle target="@ilaomianyang/dsh-memory-graph"`。**记忆库不会被删**,它在 `~/dsh-memory`,独立于插件。
92
92
 
93
+ ## 它自己更新
94
+
95
+ 从 npm 装的那份,设置页最上面有「版本与更新」一行:当前版本号、**检查更新**、**立即更新**。
96
+
97
+ ![设置里的「版本与更新」:当前版本、检查更新、立即更新,下面是四个开关](docs/update-settings.png)
98
+
99
+ 上面这张是真机(开发机上是 `link:` 安装,所以「更新」是灰的,旁边就写着原因):版本号、是否最新、用的哪个源、上次检查时间,下面四个开关是 `update.*`。
100
+
101
+ - 插件启动时自己查一次,之后每 `update.checkEveryHours` 小时(默认 12)最多查一次。查的是 `<注册表>/@ilaomianyang%2fdsh-memory-graph/latest`——一次 GET,不带查询串、不带你的记忆库路径、不带任何对话内容。结果记在库里的 `.update-state.json`,离线时保留上一次的答案,不会把"查不到"显示成"已是最新"。
102
+ - 点「立即更新」才装。安装走宿主自己的 `pluginManager.installBundle`,和 `dsh plugin` / 插件管理里那条路是同一条,尊重你配的 registry,不再造第二个包管理器。
103
+ - **`link:` 本地安装会拒绝更新**,不是报错,是明说:"这会覆盖你的开发目录"。从源码装的那份,更新就是 `git pull` + 重载插件,见上一节。
104
+ - 装完通常要重启 Harness 才生效(和 `client.js` / `package.json` 同一类原因)。页面会直接写「需要重启 Harness 生效」,不会假装已经好了。
105
+ - `update.autoInstall`(默认关):启动时发现新版本就自己装,不用点。它在任何对话开始之前动手,是唯一"换掉插件不会打断你正在说的话"的时刻。`link:` 安装同样拒绝。
106
+ - `memory_stats` 也会报一行版本,模型想知道自己手上是哪一版时不用猜。
107
+
108
+ 不想让它联网就把 `update.enabled` 关掉:那之后一次请求都不发,页面只显示当前版本。
109
+
93
110
  ## 你的记忆库长什么样
94
111
 
95
112
  ```
@@ -137,6 +154,13 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
137
154
  retrieval:
138
155
  limit: 8
139
156
  hops: 1
157
+ auto:
158
+ enabled: true
159
+ catchUp: true # 有积压就一批批追平(见下)
160
+ maxBatchesPerDay: 6
161
+ update:
162
+ enabled: true # 启动时查一次有没有新版本
163
+ autoInstall: false
140
164
  ```
141
165
 
142
166
  | 键 | 默认 | 说明 | 在设置页 |
@@ -175,37 +199,52 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
175
199
  | `auto.minGapMinutes` | `30` | 两条规则共用:两次自动蒸馏至少隔多久 | ✓ |
176
200
  | `auto.quietHours` | `''` | 不跑的时间段(如 `23:00-08:00`,可跨午夜) | ✓ |
177
201
  | `auto.project` | `''` | 自动蒸馏出的卡片记到哪个项目 | ✓ |
202
+ | `auto.catchUp` | `false` | 追平模式:还攒着一整批就一批批蒸下去,直到追平 | ✓ |
203
+ | `auto.catchUpEveryMinutes` | `10` | 追平模式每批之间隔多少分钟(自成一套节奏) | ✓ |
204
+ | `auto.maxBatchesPerDay` | `6` | 一天最多自动蒸几批,0 = 不限;这是自动花钱的唯一上限 | ✓ |
205
+ | `update.enabled` | `true` | 定期问 npm 有没有新版本(只查,不装,不带任何记忆库信息) | ✓ |
206
+ | `update.checkEveryHours` | `12` | 查到的结果保持多久,期间不重复请求 | ✓ |
207
+ | `update.autoInstall` | `false` | 启动时发现新版本就自己装(`link:` 安装会拒绝) | ✓ |
208
+ | `update.registry` | `''` | 用哪个 npm 源查;空 = 宿主的源,再退到 npmjs | ✓ |
178
209
  | `retrieval.limit` | `8` | 一次检索最多返回几张卡片 | ✓ |
179
210
  | `retrieval.hops` | `1` | 联想扩散跳数,0 = 只做关键词 | ✓ |
180
211
  | `retrieval.snippetChars` | `320` | 每条结果给模型看多少字摘要 | ✓ |
181
212
  | `retrieval.timelineLimit` | `20` | `memory_timeline` 默认回看多少条 | ✓ |
182
213
 
183
- 为什么只有这 34 个能在这页改?Harness 的设置机制只投影**声明为 volatile 的字段**,也就是"每次用到时才读"的那些。上面带 ✓ 的都是这种:改一次,下一次调用就换了。而 `enabled` / `vaultDir` / `defaultProject` / `crossProject` 是在激活时消费的,改了得重载插件,所以不放进页面装样子——页面底部会直接把这件事写出来,而不是给你几个点了没反应的开关。
214
+ 为什么只有这 41 个能在这页改?Harness 的设置机制只投影**声明为 volatile 的字段**,也就是"每次用到时才读"的那些。上面带 ✓ 的都是这种:改一次,下一次调用就换了。而 `enabled` / `vaultDir` / `defaultProject` / `crossProject` 是在激活时消费的,改了得重载插件,所以不放进页面装样子——页面底部会直接把这件事写出来,而不是给你几个点了没反应的开关。
184
215
 
185
- ### 什么时候它会自己蒸:两条规则
216
+ ### 什么时候它会自己蒸:三条规则
186
217
 
187
218
  默认**不自动**。打开 `auto.enabled` 之后,插件每分钟看一次「现在该不该蒸」——这一眼只做算术(未蒸馏条数、距上次多久、是不是静默时段),**不调模型、不花钱**。命中规则才真的跑一次,跑的还是同一个 `distill()`。
188
219
 
189
- 两条规则互相独立,可以只开一条,也可以都开:
220
+ 三条规则互相独立,可以只开一条,也可以都开:
190
221
 
191
222
  | 规则 | 配置 | 什么时候触发 |
192
223
  | --- | --- | --- |
193
224
  | 攒够就蒸 | `auto.minEpisodes` | 未蒸馏情节 ≥ 这个数(比如 10 条) |
194
225
  | 定时蒸 | `auto.everyMinutes` | 距上次自动蒸馏 ≥ 这么多分钟(1440 = 每天一次) |
226
+ | 追平 | `auto.catchUp` | 还攒着**一整批**(≥ `maxEpisodesPerRun` 条)时就一批批蒸下去,每批之间隔 `auto.catchUpEveryMinutes`(默认 10),一天最多 `auto.maxBatchesPerDay` 批(默认 6,0 = 不限) |
227
+
228
+ 「追平」是给**积压**用的:导入了历史会话、或者一周没开机回来,未蒸馏的可能有两千条——前两条规则一次只消化一批,靠它们要等几十天。打开追平,它会自己一批批往前推,直到剩下不到一整批为止,然后安安静静等下一次。
229
+
230
+ - 它**自己**有节奏(`catchUpEveryMinutes`),不受 `minGapMinutes` 限制;另外两条规则仍然受。
231
+ - `maxBatchesPerDay` 是唯一给自动花钱**设上限**的旋钮:默认 6 批 ≈ 240 条情节/天。想快点追平就调大,不想它半夜跑就把 `quietHours` 设上。
232
+ - 追平跑到一半被打断(重启、关机、锁被别人拿着)没关系:计数在 `.auto-state.json` 里,第二天(或下一个间隔)继续,已经蒸过的不会重蒸。
233
+ - 2080 条积压 + 默认配置:6 批/天 × 40 条 ≈ 9 天追平;想一天追完就把上限调到 50。
195
234
 
196
- 另外三个闸门,两条规则共用:
235
+ 另外三个闸门,规则之间共用:
197
236
 
198
- - `auto.minGapMinutes`(默认 30):两次自动蒸馏至少隔这么久。它先于两条规则判断——刚攒够就被反复问是没意义的,而且每次问都要花钱。
199
- - `auto.quietHours`(默认空):这段时间里不跑,可以跨午夜(`23:00-08:00`)。写不对就当没写,不会因此永远不跑。
237
+ - `auto.minGapMinutes`(默认 30):两条**非追平**规则共用:两次自动蒸馏至少隔这么久。它先于那两条规则判断——刚攒够就被反复问是没意义的,而且每次问都要花钱。
238
+ - `auto.quietHours`(默认空):这段时间里不跑,可以跨午夜(`23:00-08:00`)。写不对就当没写,不会因此永远不跑。追平也听它。
200
239
  - `auto.project`(默认空):自动蒸出来的卡片记到哪个项目;留空用最近那条情节的项目。
201
240
 
202
241
  几个刻意的选择:
203
242
 
204
243
  - **定时蒸的时钟从打开那一刻算起**:刚打开不会立刻跑一次——「每天一次」不该在你按下开关的瞬间就花掉一次调用。时钟单独存(`startedAt`),不当作"上次运行",所以它不会顺带把「攒够就蒸」推迟一个最短间隔。
205
- - **间隔在调用之前就记下**,所以一次跑空了(没有新卡片)也算数,不会因为"没产出"而在下一分钟再问一遍。记录在库里的 `.auto-state.json`(只有 `lastAttemptAt`),**重启不重置**。
244
+ - **间隔在调用之前就记下**,所以一次跑空了(没有新卡片)也算数,不会因为"没产出"而在下一分钟再问一遍。追平的批次计数同理:调用之前就 +1,失败也算用掉一个名额——上限才是真的上限。记录在库里的 `.auto-state.json`(`lastAttemptAt` / `startedAt` / `batchDay` / `batchesToday`),**重启不重置**。
206
245
  - 它和 `.distill-state.json` 是两件事:后者是**游标**(哪些行已经读过),前者是**尝试时间**。混在一起会让"读过了但没产出"变得无法表达。
207
246
  - 自动、按钮、`memory_distill` 共用**一把锁**:正在跑的时候,第二条路拿到的是"已经有一次在跑",不会排队、不会两份一起花。
208
- - 想关掉:`auto.enabled=false`(总开关),或者把两条规则都设 0(留着开关但没规则,等于不跑)。
247
+ - 想关掉:`auto.enabled=false`(总开关),或者把规则都设 0(留着开关但没规则,等于不跑)。
209
248
 
210
249
  ![设置里的「自动蒸馏」:开关、攒够就蒸、定时蒸、最短间隔、静默时段、记到哪个项目](docs/auto-settings.png)
211
250
 
@@ -281,7 +320,7 @@ memory_backfill {dryRun: true} → 133 个会话 / 将写入 2078 条情节
281
320
  memory_backfill {dryRun: false} → 已写入 2078 条情节;已存在跳过 85 条
282
321
  ```
283
322
 
284
- 回填出来的是**情节**,还没变成卡片:蒸馏一次(`memory_distill`、设置页按钮,或让自动蒸馏一天跑一批,默认每批 40 条)才会进检索层。2000 条情节要花的就是 50 批模型调用,这件事该由你决定什么时候花。
323
+ 回填出来的是**情节**,还没变成卡片:蒸馏一次(`memory_distill`、设置页按钮,或让自动蒸馏一天跑一批,默认每批 40 条)才会进检索层。2000 条情节要花的就是 50 批模型调用,这件事该由你决定什么时候花——想让它自己慢慢消化完,打开 `auto.catchUp`(追平模式):它一天最多跑 `auto.maxBatchesPerDay` 批,追平就自己停。回填结束时会顺手把 `README.md` 和 `graph.html` 按新情节重画一次,所以打开这两个文件看到的是当下的库,不是导入前的样子。
285
324
 
286
325
  会话文件是**拼接的 zstd 容器**(每次追加写一帧),所以读它要先把帧切开——`src/history.js` 里有一个和 harness 自己用的算法同构的帧扫描器。
287
326
 
@@ -397,7 +436,7 @@ Harness 会把运行时快照、技能目录、时间戳、审批通知、压缩
397
436
  零依赖,不联网,不碰真实记忆库:
398
437
 
399
438
  ```bash
400
- npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 19 + 12 = 244 项检查,另加 25 题检索评测
439
+ npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 25 + 12 + 27 = 277 项检查,另加 25 题检索评测
401
440
  npm run eval # 只跑检索评测(离线,确定性的)
402
441
  ```
403
442
 
@@ -409,7 +448,8 @@ npm run eval # 只跑检索评测(离线,确定性的)
409
448
  - `test/tools.mjs` — 逐个跑 9 个工具的 `execute` + `render`;另外验证模型要的那次蒸馏走的是共享 runner(而不是绕开它直接调 `distill`),否则它可能和一次自动蒸馏撞在一起
410
449
  - `test/page.mjs` — 侧栏面板背后那条路由:`/api/memory-graph.page` 在受认证通道上、空库也给一页而不是报错、**和落到磁盘的 `graph.html` 是同一份渲染**(只差时间戳)、`X-Frame-Options: SAMEORIGIN` 所以只有应用能嵌、库读坏了把原因印在页面上(转义后)、没有库是 500 不是崩溃、页面里不带任何库路径
411
450
  - `test/distillapi.mjs` — 「蒸馏一次」那条路:`/api/memory-graph.distill` 在受认证的 `/api` 通道上、跑的是同一个 `distill()`(活库 + 设置里的模型 + 打码规则)、一次按下一次运行、并发第二次被拒(不排队)、抽取器的拒绝是 200 而不是 500、抛错是 500 且锁照样解开、写了卡片才重建视图、渲染失败只 warn、卡片列表截到 40、没有 runner 时回 500 而不是把异常扔给浏览器
412
- - `test/auto.mjs` — 自动蒸馏:两条规则各自触发、都开时谁先满足谁先跑、最短间隔先于两条规则判断、静默时段跨午夜、tick 只做决定(关着时连库都不读)、一次 tick 最多一次运行、间隔在调用前就记下、重启后读回 `.auto-state.json` 所以不会重跑、跑空了也记间隔、正在跑时不叠加、失败照原因记日志、项目名透传、库读不了是跳过不是崩、阈值大时请求的条数够看到它
451
+ - `test/auto.mjs` — 自动蒸馏:三条规则各自触发、都开时谁先满足谁先跑、最短间隔先于两条非追平规则判断、静默时段跨午夜且压过追平、tick 只做决定(关着时连库都不读)、一次 tick 最多一次运行、间隔在调用前就记下、重启后读回 `.auto-state.json` 所以不会重跑(追平的每日批次计数也一样读回来)、跑空了也记间隔、正在跑时不叠加、失败照原因记日志、项目名透传、库读不了是跳过不是崩、阈值大时请求的条数够看到它;追平单独一组:不足一整批不算积压、按自己的节奏(10 分钟)而不是 30 分钟的间隔、到每日上限就停、第二天重置
452
+ - `test/update.mjs` — 自我更新:版本比较(预发布低于正式、`v` 前缀)、`link:`/`file:` 安装被识别成"工作副本"因而拒绝更新、注册表版本与状态行的中文说法、窗口内只问一次注册表、`force` 一分钟内不重复请求、缓存到期自己失效、离线时保留上一次答案而不是显示"已是最新"、关掉检查就一次请求都不发、配置的源优先于宿主的源、宿主没有 pluginManager 也不影响检查、安装只走 `installBundle` 且要的是确切版本、装成功/需重启/失败各自的说法、正在装的时候第二次拒绝而不是排队、`autoInstall` 只在允许且是注册表安装时动手、路由层 GET 不缓存 / POST 是唯一能装的方法 / 没有 checker 时 503
413
453
  - `test/extract.mjs` — 抽取器:`fetch` 打桩验证 http 路线(提示词构造、key 解析顺序、JSON 解析容错、每张卡片归到自己的项目、游标前进、dry run 不落盘、密钥在写入前被拦住),再用一个假的 `llm` 服务验证 harness 路线(借用设置里的模型与思考档、显式覆盖、没有选中模型时的报错、`finish` 各种终态、取消、空回复、以及全程不碰 `fetch`),最后验证分批:按输出上限折算每批条数、长后台分多次调用、被截断就对半拆、某一批失败时前面几批的卡片留着、`dryRun` 只预览第一批
414
454
 
415
455
  下面两个测试会真的联网/花钱,所以不在 `npm test` 里,要自己跑:
@@ -436,7 +476,7 @@ npm version minor -m "chore(release): v%s" && git push --follow-tags # minor /
436
476
 
437
477
  1. 核对 tag 和 `package.json` 的版本号——不一致就失败,不会发出一个版本号对不上的包;
438
478
  2. 查这个版本是不是已经在注册表上——在就跳过,重推同一个 tag 是空操作而不是报错;
439
- 3. `npm publish --provenance --access public`(`prepublishOnly` 会再跑一遍那 244 项,红的进不去);
479
+ 3. `npm publish --provenance --access public`(`prepublishOnly` 会再跑一遍那 277 项,红的进不去);
440
480
  4. npm 页面上多一条「Built and signed on GitHub Actions」,证明这个 tarball 是从这个仓库的这个提交构建的。
441
481
 
442
482
  凭据有两条路,工作流两条都认(有 `NPM_TOKEN` 就用它,没有就走 OIDC):