dsh-hypatia-auto-memory 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,17 @@ additions are used when the binary has them and skipped when it does not:
33
33
  The first call logs which the binary offers
34
34
  (`hypatia over mcp: no-embed yes, similar filters yes`).
35
35
 
36
+ The plugin also needs **dsh ≥ 0.1.7**, and dsh enforces that itself rather than
37
+ this README: the package declares
38
+ `peerDependencies["@deepseek-ai/dsh"]: ">=0.1.7-rc.1"` (the same range also
39
+ sits under `dsh.engines.dsh` for display), and dsh checks every
40
+ `@deepseek-ai/dsh*` peer against the running version when a bundle is installed
41
+ and again at startup — an older runtime refuses the install instead of loading a
42
+ plugin built for API it does not have, and `dsh plugin allow-version` grants an
43
+ exact-version exemption when the risk is accepted deliberately. The peer is
44
+ marked optional in `peerDependenciesMeta` so that neither npm nor a profile
45
+ installation pulls the runtime in as a dependency: the launcher supplies it.
46
+
36
47
  ```bash
37
48
  # from a local checkout (development)
38
49
  dsh plugin --profile web add link:/path/to/dsh-hypatia-auto-memory
@@ -85,8 +96,11 @@ session/event (emit)
85
96
  │ └─ cascade ──▶ $not-summaried ──▶ sum<N>-* (summary N)
86
97
  └─ session/title | compaction/summary ──▶ session-node ──▶ session-<sid> + belongTo
87
98
  session/disposed ──▶ final flush + consolidation, thresholds waived (a session
88
- closed while DSH keeps running; a restart is covered by the
89
- startup consolidation backfill instead)
99
+ closed while DSH keeps running)
100
+ collect fiber dispose ──▶ the same flush for every session still open, persisted
101
+ before the queue stops; the model call finishes at the next start
102
+ (a restart, a config-change restart, or a crash: the startup
103
+ consolidation backfill)
90
104
  agent/session-start ──▶ rules/taboos inject()
91
105
  ```
92
106
 
@@ -178,6 +192,19 @@ agent/session-start ──▶ rules/taboos inject()
178
192
  and advances the watermark only that far, so the remainder is not silently
179
193
  marked done.
180
194
 
195
+ The extraction call asks for **no reasoning** on a route that advertises it.
196
+ Extraction is a mechanical transform into a fixed JSON shape, and on a
197
+ thinking route the reasoning is charged to the SAME `maxOutputTokens` as the
198
+ answer: measured live, a `deepseek-flash` route resolving to `high` spent
199
+ ~800–1400 of a 2000-token cap thinking and hit `max-tokens` with the JSON half
200
+ written (9.3 s, against 25 s for a completed run of the same kind on that
201
+ route). Where a route cannot be asked — or a provider ignores the control —
202
+ the budget carries a fixed thinking allowance instead, and a truncation is
203
+ **retried** rather than declared permanent: the retry asks for one work unit
204
+ instead of `maxWorkUnitsPerRun`, because the units are what make the reply
205
+ long. Cascade is the exception: it keeps thinking (it is compressing sixteen
206
+ summaries, not filling a shape) and pays for it in the same allowance.
207
+
181
208
  - **Cascade** archives sixteen unarchived tier-N summaries into one tier-(N+1)
182
209
  entry, using hypatia's own `$not-summaried` (which anti-joins on
183
210
  `statement.tail` and orders `created_at ASC`, giving FIFO batching for free).
@@ -200,22 +227,39 @@ agent/session-start ──▶ rules/taboos inject()
200
227
  re-log and a re-consolidation, so both passes act only on positive evidence:
201
228
  a failed shelf query or an empty session listing changes nothing.
202
229
 
203
- A third pass consolidates what a restart interrupted. The session-end trigger
204
- does not survive one: DSH runs its close path at shutdown (it appends
205
- `session/end-seed`), but nothing queued there becomes durable before the
206
- process is gone — measured on a live restart, the storage file was not written
207
- and the session came back with `lastConsolidatedSeq: 0`. So a row whose logged
208
- tail was never consolidated is queued at the next startup, gated by the same
209
- `consolidation.minNewTokens` floor and read from the row rather than by
210
- loading the session. Without that gate every restart would spend one model
211
- call per session carrying any tail at all.
230
+ A third pass consolidates what a shutdown could only persist, and what a crash
231
+ or `kill -9` left behind. Nothing a closing session owes survives on its own:
232
+ `session/disposed` observers are fire-and-forget in DSH, the collect fiber is
233
+ torn down before the session store releases its sessions, and the queue dies
234
+ with the fiber — so an `enqueue` there was a silent no-op and a whole run's
235
+ tails were dropped. Two things fix that. The collect fiber's disposer now
236
+ **persists** both ranges for every session still open (registering the sweep
237
+ after the queue's own disposer, because cordis disposes a fiber's effects in
238
+ reverse order, so it runs while the queue can still write), and this pass
239
+ finishes the work at the next start. Only the final log write is waited for at
240
+ shutdown: a hypatia write is milliseconds, while the model call it may trigger
241
+ is measured in tens of seconds and could never fit inside DSH's 5 s shutdown
242
+ budget.
243
+
244
+ This pass deliberately ignores the `consolidation.minNewTokens` floor that
245
+ paces the live trigger. The floor is right while a conversation is running —
246
+ there is always a later turn — but at boot there is no later turn, and a tail
247
+ skipped here is skipped for good: measured on a live shelf, fifteen sessions
248
+ carried 1–1950 pending tokens, not one of them was ever consolidated, and
249
+ eight of nine sessions had messages with `lastConsolidatedSeq: 0`. Cost is
250
+ bounded by `housekeeping.consolidationBackfillPerScope` instead — at most that
251
+ many sessions **per project scope** per start, the largest tails first, so a
252
+ backlog drains over several starts rather than in one unbounded burst of model
253
+ calls. A range whose consolidate record is `failed` is taken first whatever its
254
+ size: `resumeKind` never re-schedules a failed row, and re-enqueueing it is the
255
+ only path back.
212
256
 
213
257
  Those tasks then read their events from **storage**, not only from the live
214
258
  store. `session/created` — the signal that wakes a deferred task — fires only
215
259
  where an agent actually runs, so a finished session may never emit it again,
216
260
  and a subagent session (which the sidebar does not even list) almost certainly
217
261
  will not: one sat `deferred` across a restart and stayed deferred while its
218
- transcript was open on screen. `sessionPersistence.load()` returns the same
262
+ transcript was open on screen. `sessionPersistence.open(id, 'read')` returns the same
219
263
  log without publishing anything, and the executors use only `snapshotEvents`,
220
264
  `inheritedEventCount`, `seq` and `header`, so a tail nothing will reopen still
221
265
  becomes knowledge. A composition without `sessionPersistence` behaves as
@@ -304,9 +348,13 @@ agent/session-start ──▶ rules/taboos inject()
304
348
 
305
349
  ## Configuration
306
350
 
307
- The plugin's cordis `Config` is its settings tab (dsh ≥ 0.1.7 projects a
308
- plugin's Config schema into Settings → Plugins; every field below applies on
309
- save, without a profile reload). Stored as the plugin entry's `config:` in the
351
+ The plugin's cordis `Config` is its configuration form. dsh ≥ 0.1.7 projects a
352
+ plugin's Config schema onto the sidebar's **Plugins** page, and the card renders
353
+ on this bundle's own page there; a deployment whose profile renders no Plugins
354
+ page gets the same card as a tab under **Settings → Built-in plugins**. The card
355
+ takes whichever seat exists and moves between them without ever appearing twice
356
+ (`src/client/plugin-card-seat.ts`). Every field below applies on
357
+ save, without a profile reload. Stored as the plugin entry's `config:` in the
310
358
  profile patch; a pre-0.1.7 `settings.yaml` section is imported once on first
311
359
  launch. All fields optional, defaults shown. A blank consolidation route is
312
360
  refused on save; a duplicate one is dropped at runtime with a warning:
@@ -353,6 +401,7 @@ hypatia-auto-memory:
353
401
  reconcileOnStartup: true # reset rows whose session has no msg-* left in the shelf
354
402
  pruneVanishedSessions: true # drop rows and tasks of sessions DSH no longer has
355
403
  backfillConsolidation: true # consolidate a logged tail the in-session trigger never reached
404
+ consolidationBackfillPerScope: 4 # sessions that pass may queue per scope, per start
356
405
  ```
357
406
 
358
407
  The cordis config block on the bundle row only carries skill packaging:
@@ -453,7 +502,7 @@ now only projects plugin Config forms.)
453
502
  | Summaries but no `sum2-*` | Fewer than `cascade.batchSize` unarchived tier-1 summaries in that project yet |
454
503
  | Work units have no relationships | No embedding model on the shelf (`similar` fails), every candidate was beyond `dedupMaxDistance`, or the adjudication call itself produced no verdict — `model_calls` shows that as `incomplete` / `max-tokens`, which is what a thinking-enabled route does when it spends the whole output budget on reasoning before answering. The call now asks a route for `reasoningEffort: 'off'` once the adapter advertises it, and carries a 1024-token cap for routes that cannot honour it. Since hypatia #19 the local model is looked up in `~/.hypatia/models/<org>/<name>` (or the Hugging Face cache), no longer beside the shelf: a shelf that used to answer `similar` and now says `is not installed` needs `hypatia model install <model>`, or `hypatia model register <model> <dir>` pointing at the files it already has |
455
504
  | `similar` still returns `msg-*` rows | They were written before this plugin opted the log layer out of embedding, or by a hypatia without `--no-embed`. Retract the knowledge vectors once with `embedding.skip_tags = ["message"]` in the shelf's `shelf.toml` followed by `hypatia backfill` (a binary older than the key refuses to open the shelf, so upgrade every binary sharing it first). Do not add `session` to that list: `skip_tags` matches any entry carrying the tag, and knowledge people write about sessions carries it too — one such entry lost its vector on the shelf this was tried on. The plugin's own `session-*` nodes are few and now opt out per write. Statements have no tags and no update command, so `belongTo` / `summary` edges written before keep their vectors |
456
- | Task in `deferred` state | Neither the live store nor persistence could supply its session. Not an error: it spends no attempts and runs as soon as one of them can. Normally storage answers immediately — the state persists only when `sessionPersistence` is absent from the composition, or its read failed (look for `could not be read` in the log) |
505
+ | Task in `deferred` state | Neither the live store nor persistence could supply its session. Not an error: it spends no attempts and runs as soon as one of them can. Normally storage answers immediately — the state persists only when `sessionPersistence` is absent from the composition, or its read failed (look for `could not be read` in the log). A deferred consolidation is re-queued by the next startup's backfill, so it does not wait for its session to be opened again |
457
506
  | Task in `failed` state | A hypatia or model error persisted after `maxAttempts`. Failed `log-message` records are pruned at the next startup, since the watermark re-derives their range; other kinds are kept for inspection — delete one to let the next trigger re-create it |
458
507
  | Watermark says logged, but the shelf has no entries | The shelf was reset or switched after logging. Handled at startup: `housekeeping.reconcileOnStartup` resets any row whose session has no `msg-*` left, and that session is re-logged — and re-consolidated — from the start on its next activity. A shelf query that fails leaves the row untouched. A session whose messages were all deleted on purpose is indistinguishable and is logged again; turn the switch off if that matters |
459
508
  | Warning `hypatia mcp unavailable: …; using the hypatia CLI until the profile reloads` | The binary predates `hypatia mcp` (the message quotes its `unrecognized subcommand`), lacks a tool the plugin calls, or answered the handshake with an error. Everything keeps working over the CLI; upgrade hypatia and reload the profile to use MCP |
@@ -572,7 +621,8 @@ dsh-hypatia-auto-memory/
572
621
  │ ├── SettingsCard.tsx
573
622
  │ ├── MemoryView.tsx # the Memory tab body
574
623
  │ ├── memory-client.ts # its fetch + merge rules (pure)
575
- │ ├── shelves.ts # shelf dropdown choices from the inventory namespace
624
+ │ ├── plugin-card-seat.ts # card seat: official Plugins page, else Settings tab
625
+ │ ├── shelves.ts # shelf dropdown choices (listing from the /shelves route)
576
626
  │ └── slot-contract.ts
577
627
  ├── lib/
578
628
  │ └── client.js # built browser factory (commit this)
package/README.zh-CN.md CHANGED
@@ -28,6 +28,14 @@
28
28
  检索。首次调用会记录二进制提供了哪些(`hypatia over mcp: no-embed yes, similar
29
29
  filters yes`)。
30
30
 
31
+ 本插件还需要 **dsh ≥ 0.1.7**,而且这个下限由 dsh 自己强制执行,不靠本 README:
32
+ 包里声明了 `peerDependencies["@deepseek-ai/dsh"]: ">=0.1.7-rc.1"`(同样的范围也写在
33
+ `dsh.engines.dsh` 里,供展示),dsh 在安装 bundle 时、以及每次启动时,会把每个
34
+ `@deepseek-ai/dsh*` peer 与自身版本比对——运行时太旧会直接拒绝安装,而不是加载一个
35
+ 面向它并不具备的 API 写成的插件;确实要接受风险时,用 `dsh plugin allow-version`
36
+ 授予精确版本豁免。该 peer 在 `peerDependenciesMeta` 里标为 optional,这样 npm 和
37
+ profile 安装都不会把运行时本身拉成依赖:运行时由 launcher 提供。
38
+
31
39
  ```bash
32
40
  # 从本地 checkout 安装(开发)
33
41
  dsh plugin --profile web add link:/path/to/dsh-hypatia-auto-memory
@@ -75,7 +83,10 @@ session/event (emit)
75
83
  │ └─ cascade ──▶ $not-summaried ──▶ sum<N>-* (summary N)
76
84
  └─ session/title | compaction/summary ──▶ session-node ──▶ session-<sid> + belongTo
77
85
  session/disposed ──▶ final flush + consolidation, thresholds waived (DSH 仍在运行时
78
- 关闭的会话;进程重启则由启动时的 consolidation backfill 覆盖)
86
+ 关闭的会话)
87
+ collect fiber 拆除 ──▶ 对每个仍打开的会话做同样的收尾,并在队列停止前落盘;模型
88
+ 调用留到下次启动完成(重启、配置变更重启,或崩溃:启动时的
89
+ consolidation backfill)
79
90
  agent/session-start ──▶ rules/taboos inject()
80
91
  ```
81
92
 
@@ -143,6 +154,15 @@ agent/session-start ──▶ rules/taboos inject()
143
154
  脱敏并绝对化日期,超预算的 span 只巩固其**最旧**的条目,水位也只推进到那里,剩下
144
155
  的不会被打成已完成。
145
156
 
157
+ 抽取调用在**声明支持的路由上关闭推理**。抽取是朝固定 JSON 形状的机械变换,而在开启
158
+ thinking 的路由上,推理与答案**共享同一个 `maxOutputTokens`**:实测中一条解析为
159
+ `high` 的 `deepseek-flash` 路由在 2000 的预算里花掉约 800–1400 思考,然后带着写到
160
+ 一半的 JSON 撞上 `max-tokens`(9.3s,而同一条路由上同类成功调用要 25s)。路由无法
161
+ 被要求关闭(或 provider 忽略该控制)时,预算改为携带固定的思考余量;而**截断不再
162
+ 算永久失败**,而是重试——重试时只要 1 个工作单元而不是 `maxWorkUnitsPerRun`,因为
163
+ 正是工作单元让回复变长。Cascade 是例外:它保留 thinking(它是在压缩十六条摘要,不是
164
+ 在填一个形状),并用同一份余量来支付。
165
+
146
166
  - **Cascade** 把十六个未归档的 tier-N 摘要归档为一条 tier-(N+1) 条目,用 hypatia 自带
147
167
  的 `$not-summaried`(它对 `statement.tail` 做反连接并按 `created_at ASC` 排序,
148
168
  免费得到 FIFO 分批)。这就是协议的 log₁₆(n) 归档:每一层只压缩上一层已经蒸馏过的
@@ -160,17 +180,28 @@ agent/session-start ──▶ rules/taboos inject()
160
180
  DSH 已不存在的会话的行和任务。重置的代价是一次重新记录和一次重新巩固,所以两遍
161
181
  都只在有正面证据时才行动:shelf 查询失败或空的会话列表都不做任何改动。
162
182
 
163
- 第三遍巩固那些被重启打断的内容。session-end 触发器活不过重启:DSH 在关闭时跑它的
164
- 关闭路径(追加 `session/end-seed`),但那里排入的任何东西在进程消失前都来不及
165
- 持久化——实测在一次真实重启中,storage 文件根本没写,会话回来时
166
- `lastConsolidatedSeq: 0`。所以一个有已记录但从未巩固的尾部的行,会在下次启动时被
167
- 排入队列,受同样的 `consolidation.minNewTokens` 下限约束,并且从行里读取而不是
168
- 加载会话。没有这道门槛,每次重启都会为每个带尾部的会话花一次模型调用。
183
+ 第三遍巩固那些关闭时只能落盘、以及崩溃或 `kill -9` 完全没留下的内容。关闭中的会话
184
+ 所欠的东西本身活不下来:DSH 里 `session/disposed` 的观察者是 fire-and-forget,
185
+ collect fiber 先于 session store 释放会话就被拆除,而队列随 fiber 一起消失——所以在
186
+ 那里 `enqueue` 是一次静默的空操作,一整轮的尾巴就这么丢了。两件事修好了它:collect
187
+ fiber 的 disposer 现在会为每个仍打开的会话**落盘**两段范围(把收尾 effect 注册在队列
188
+ 自己的 disposer **之后**,因为 cordis 按注册逆序拆除 fiber 的 effects,于是它跑的时候
189
+ 队列还能写),而这一遍在下一次启动把活干完。关闭时只等最后的日志写入:一次 hypatia
190
+ 写入是毫秒级,而它可能触发的模型调用是几十秒,永远塞不进 DSH 5s 的关机预算。
191
+
192
+ 这一遍**刻意不看**节流实时触发器的 `consolidation.minNewTokens` 下限。对话还在进行时
193
+ 下限是对的——总还有下一轮——但启动时没有下一轮,在这里被跳过的尾部就是永久跳过:
194
+ 实测一个 live shelf 上 15 个会话带着 1–1950 的 pending token,没有一个被巩固过,9 个
195
+ 有消息的会话里 8 个 `lastConsolidatedSeq: 0`。成本改由
196
+ `housekeeping.consolidationBackfillPerScope` 封顶——每次启动**每个 project scope**
197
+ 最多这么多个会话,尾巴最大的优先,于是积压分几次启动消化,而不是一次爆出无界的模型
198
+ 调用。consolidate 记录处于 `failed` 的范围无论多小都优先:`resumeKind` 不会重排
199
+ failed 行,重新入队是它唯一的回头路。
169
200
 
170
201
  这些任务随后从 **storage** 读取事件,而不只是从活 store。`session/created`——唤醒
171
202
  被延迟任务的信号——只在 Agent 实际运行的地方触发,所以一个已结束的会话可能再也不
172
203
  会发它,而 subagent 会话(侧栏甚至不列出)几乎肯定不会:有一个就曾跨重启停留在
173
- `deferred` 状态,尽管它的 transcript 正开在屏幕上。`sessionPersistence.load()`
204
+ `deferred` 状态,尽管它的 transcript 正开在屏幕上。`sessionPersistence.open(id, 'read')`
174
205
  返回同样的日志而不发布任何东西,执行器只用 `snapshotEvents`、
175
206
  `inheritedEventCount`、`seq` 和 `header`,所以一个不会再被重新打开的尾部仍能变成
176
207
  知识。没有 `sessionPersistence` 的组合行为照旧:任务等待。
@@ -235,8 +266,11 @@ agent/session-start ──▶ rules/taboos inject()
235
266
 
236
267
  ## 配置
237
268
 
238
- 插件的 cordis `Config` 就是它的设置页(dsh ≥ 0.1.7 把插件的 Config schema 投影到
239
- 「设置 → 插件」;下面每个字段保存即生效,无需重载 profile)。配置以插件条目 `config:`
269
+ 插件的 cordis `Config` 就是它的配置表单。dsh ≥ 0.1.7 把插件的 Config schema 投影到
270
+ 侧边栏的**「插件」页**,卡片渲染在该 bundle 自己的页面上;如果某个部署的 profile
271
+ 不渲染这个页面,同一张卡片会退到**「设置 → 内置插件」**里作为一个标签页。卡片落在
272
+ 存在的那个位置上,并在两者之间迁移时绝不会同时出现(`src/client/plugin-card-seat.ts`)。
273
+ 下面每个字段保存即生效,无需重载 profile。配置以插件条目 `config:`
240
274
  的形式存进 profile patch;0.1.7 之前的 `settings.yaml` 段落会在首次启动时被一次性导入。
241
275
  所有字段可选,展示的是默认值。空白的巩固路由在保存时就被拒绝;重复的路由在运行时被丢弃
242
276
  并记一条警告:
@@ -283,6 +317,7 @@ hypatia-auto-memory:
283
317
  reconcileOnStartup: true # 重置 session 在 shelf 里已无 msg-* 的行
284
318
  pruneVanishedSessions: true # 删除 DSH 已不存在的会话的行和任务
285
319
  backfillConsolidation: true # 巩固会话内触发器从未到达的已记录尾部
320
+ consolidationBackfillPerScope: 4 # 每次启动每个 scope 允许排入的会话数
286
321
  ```
287
322
 
288
323
  bundle 行上的 cordis config 块只承载技能打包:
@@ -364,7 +399,7 @@ shelf 清单本来就不是配置,而设置域现在只投影插件的 Config
364
399
  | 有摘要但没有 `sum2-*` | 该项目里未归档的 tier-1 摘要还不足 `cascade.batchSize` 条 |
365
400
  | 工作单元没有关系 | shelf 没有嵌入模型(`similar` 失败),所有候选都超过了 `dedupMaxDistance`,或裁决调用本身没有产出裁决——`model_calls` 里表现为 `incomplete` / `max-tokens`,也就是开启了思考的路由把整个输出预算花在推理上、还没开始作答就撞了上限。现在裁决会先确认适配器声明了 `off`,再对该路由请求 `reasoningEffort: 'off'`;对无法接受它的路由则把上限提到 1024。自 hypatia #19 起,本地模型在 `~/.hypatia/models/<org>/<name>`(或 Hugging Face 缓存)里找,不再挨着 shelf:一个以前能回答 `similar`、现在说 `is not installed` 的 shelf,需要 `hypatia model install <model>`,或 `hypatia model register <model> <dir>` 指向它已有的文件 |
366
401
  | `similar` 仍返回 `msg-*` 行 | 它们是在本插件让日志层退出嵌入之前写的,或由一个没有 `--no-embed` 的 hypatia 写的。在 shelf 的 `shelf.toml` 里用 `embedding.skip_tags = ["message"]` 加一次 `hypatia backfill` 一次性收回知识向量(比该 key 旧的二进制会拒绝打开 shelf,所以先升级所有共享它的二进制)。不要把 `session` 加进那个列表:`skip_tags` 匹配任何带该 tag 的条目,而人们写的关于会话的知识也带它——在试过的 shelf 上就有一条这样的条目丢了向量。插件自己的 `session-*` 节点很少,现在已逐次写入退出。三元组没有 tags 也没有 update 命令,所以之前写的 `belongTo` / `summary` 边保留向量 |
367
- | 任务处于 `deferred` 状态 | live store 和 persistence 都没能提供它的会话。不是错误:它不消耗尝试次数,一旦二者之一能提供就运行。通常 storage 立即回答——只有组合里没有 `sessionPersistence`,或它的读取失败(日志里找 `could not be read`)时才会持久化这个状态 |
402
+ | 任务处于 `deferred` 状态 | live store 和 persistence 都没能提供它的会话。不是错误:它不消耗尝试次数,一旦二者之一能提供就运行。通常 storage 立即回答——只有组合里没有 `sessionPersistence`,或它的读取失败(日志里找 `could not be read`)时才会持久化这个状态。被 deferred 的巩固任务会在下次启动时被 backfill 重新入队,所以它不必等会话被重新打开 |
368
403
  | 任务处于 `failed` 状态 | 一次 hypatia 或模型错误在 `maxAttempts` 之后持久化了。失败的 `log-message` 记录在下次启动时被修剪,因为水位会重新推导它们的范围;其它类型保留供检查——删掉一条好让下一个触发器重新创建它 |
369
404
  | 水位说已记录,但 shelf 里没条目 | shelf 在记录后被重置或切换。启动时处理:`housekeeping.reconcileOnStartup` 重置任何 session 已无 `msg-*` 的行,该会话下次活跃时从头重新记录——并重新巩固。shelf 查询失败则不动该行。一个所有消息都被故意删除的会话无法区分,会被重新记录;如果这要紧,关掉这个开关 |
370
405
  | 警告 `hypatia mcp unavailable: …; using the hypatia CLI until the profile reloads` | 二进制早于 `hypatia mcp`(消息会引用它的 `unrecognized subcommand`)、缺本插件调用的某工具、或握手返回错误。一切继续走 CLI;升级 hypatia 并重载 profile 以使用 MCP |
@@ -461,6 +496,7 @@ dsh-hypatia-auto-memory/
461
496
  │ ├── SettingsCard.tsx
462
497
  │ ├── MemoryView.tsx # 记忆标签页主体
463
498
  │ ├── memory-client.ts # 它的请求与合并规则(纯函数)
499
+ │ ├── plugin-card-seat.ts # 卡片落座:官方「插件」页,否则设置标签页
464
500
  │ ├── shelves.ts # shelf 下拉选项(清单来自 /shelves 路由)
465
501
  │ └── slot-contract.ts
466
502
  ├── lib/
package/cordis.patch.yml CHANGED
@@ -3,8 +3,10 @@
3
3
  # dsh plugin --profile <name> add dsh-hypatia-auto-memory (registry)
4
4
  # dsh plugin --profile <name> add link:/path/to/dsh-hypatia-auto-memory
5
5
  #
6
- # Behavior is configured through this entry's `config:` (edited live in
7
- # Settings → Plugins; dsh ≥ 0.1.7 projects the plugin's Config schema there).
6
+ # Behavior is configured through this entry's `config:` (edited live on the
7
+ # bundle's own page of the sidebar's Plugins page, which dsh ≥ 0.1.7 projects
8
+ # the plugin's Config schema onto; a deployment that renders no Plugins page
9
+ # gets the same card under Settings → Built-in plugins).
8
10
  # The cordis-level knobs below are the non-volatile ones — changing them
9
11
  # remounts the plugin:
10
12
  #
package/lib/client.js CHANGED
@@ -307,7 +307,8 @@ window.__ModuleLoader__.load({
307
307
  //#endregion
308
308
  //#region src/client/SettingsCard.tsx
309
309
  /**
310
- * Settings card for the `hypatia-auto-memory` namespace.
310
+ * Configuration card for the `hypatia-auto-memory` namespace, rendered on
311
+ * whichever seat the harness offers (see `plugin-card-seat.ts`).
311
312
  *
312
313
  * The card owns its disclosure state and stages edits until Save. Its visual
313
314
  * treatment mirrors the DSH plugin-settings cards while remaining bundle-local.
@@ -1678,7 +1679,123 @@ window.__ModuleLoader__.load({
1678
1679
  });
1679
1680
  }
1680
1681
  //#endregion
1682
+ //#region src/client/plugin-card-seat.ts
1683
+ /**
1684
+ * Which seat the settings card occupies on the harness that is running.
1685
+ *
1686
+ * dsh 0.1.7 moved a bundle's own configuration out of Settings. The sidebar's
1687
+ * Plugins page owns it now, and `plugins.bundle.config` — keyed by the bundle's
1688
+ * package name — is the seat that page renders on the bundle's own page. The
1689
+ * Settings section still declares `settings.plugins.tab`, and it is the only
1690
+ * seat a deployment whose profile does not mount the plugin manager page has.
1691
+ *
1692
+ * Three facts about the slot registry shape this module:
1693
+ *
1694
+ * - The harness declares the official seats before an external plugin's
1695
+ * `apply()` runs, so the first attempt normally succeeds. It is a plain
1696
+ * `register`, never a declaration probe: `register` is the operation the
1697
+ * registry validates, and a probe would answer for a different question —
1698
+ * "is this key declared?" is true even in the deployment whose whole point is
1699
+ * the other seat.
1700
+ * - `register` throws for an undeclared seat. That refusal is the fallback
1701
+ * signal, so the seats are tried rather than predicted.
1702
+ * - `slots/changed` is emitted synchronously from inside both `register` and a
1703
+ * disposer, so an unguarded reconcile re-enters itself mid-move and registers
1704
+ * the card twice into the seat it is leaving ("already has an entry for key
1705
+ * …"). The latch below is what prevents that.
1706
+ *
1707
+ * The card is therefore in exactly one seat at any time: the official one when
1708
+ * the harness renders it, the Settings tab otherwise, and the Settings entry is
1709
+ * disposed before the official one is taken. This module holds the policy and
1710
+ * no client-SDK import, so the moves are unit-tested with a fake registry.
1711
+ *
1712
+ * @module dsh-hypatia-auto-memory/client/plugin-card-seat
1713
+ */
1714
+ /** The official keyed seat on the sidebar Plugins page, keyed by bundle package name. */
1715
+ const BUNDLE_CONFIG_SEAT = "plugins.bundle.config";
1716
+ /** The list seat inside Settings → Built-in plugins, declared by the Settings section. */
1717
+ const SETTINGS_TAB_SEAT = "settings.plugins.tab";
1718
+ /**
1719
+ * Install one card into the seat this harness renders, following the harness if
1720
+ * the seat becomes available later.
1721
+ *
1722
+ * @param host - the client context's slot registry and event bus.
1723
+ * @param seats - the two seat contributions and an optional refusal diagnostic.
1724
+ * @returns one disposer that removes the card and stops watching.
1725
+ */
1726
+ function installPluginCard(host, seats) {
1727
+ /** The live official registration, while the card occupies that seat. */
1728
+ let official;
1729
+ /** The live Settings-tab registration, while the card occupies that seat instead. */
1730
+ let settingsTab;
1731
+ /** Set by the returned disposer: every later reconcile is a no-op. */
1732
+ let stopped = false;
1733
+ /** Re-entrancy latch: the registry emits `slots/changed` from inside register and dispose. */
1734
+ let reconciling = false;
1735
+ const refused = /* @__PURE__ */ new Set();
1736
+ /** One registration attempt, reporting its refusal once instead of on every retry. */
1737
+ const attempt = (seat, register) => {
1738
+ try {
1739
+ return register();
1740
+ } catch (error) {
1741
+ if (!refused.has(seat)) {
1742
+ refused.add(seat);
1743
+ seats.onRefused?.(seat, error);
1744
+ }
1745
+ return;
1746
+ }
1747
+ };
1748
+ /** Take the official seat when it is declared, retiring the Settings fallback first. */
1749
+ const reconcile = () => {
1750
+ if (stopped || reconciling || official !== void 0) return;
1751
+ reconciling = true;
1752
+ try {
1753
+ const dispose = attempt(BUNDLE_CONFIG_SEAT, seats.official);
1754
+ if (dispose === void 0) return;
1755
+ official = dispose;
1756
+ const previous = settingsTab;
1757
+ settingsTab = void 0;
1758
+ previous?.();
1759
+ } finally {
1760
+ reconciling = false;
1761
+ }
1762
+ };
1763
+ reconcile();
1764
+ const stopWaiting = host.slots.inject(SETTINGS_TAB_SEAT, () => {
1765
+ if (stopped || official !== void 0 || settingsTab !== void 0) return () => {};
1766
+ const dispose = attempt(SETTINGS_TAB_SEAT, seats.settingsTab);
1767
+ if (dispose === void 0) return () => {};
1768
+ settingsTab = dispose;
1769
+ if (official !== void 0) {
1770
+ settingsTab = void 0;
1771
+ dispose();
1772
+ return () => {};
1773
+ }
1774
+ return () => {
1775
+ const current = settingsTab;
1776
+ settingsTab = void 0;
1777
+ current?.();
1778
+ };
1779
+ });
1780
+ const stopWatching = host.on?.("slots/changed", () => {
1781
+ reconcile();
1782
+ });
1783
+ return () => {
1784
+ stopped = true;
1785
+ stopWatching?.();
1786
+ stopWaiting();
1787
+ const officialDispose = official;
1788
+ official = void 0;
1789
+ officialDispose?.();
1790
+ const settingsDispose = settingsTab;
1791
+ settingsTab = void 0;
1792
+ settingsDispose?.();
1793
+ };
1794
+ }
1795
+ //#endregion
1681
1796
  //#region src/client/index.tsx
1797
+ /** The bundle's npm package name: the key the official seat identifies it by. */
1798
+ const BUNDLE_NAME = "dsh-hypatia-auto-memory";
1682
1799
  const APPLY_CLAIM = "__dshHypatiaAutoMemoryApplied";
1683
1800
  function claimApply() {
1684
1801
  const scope = globalThis;
@@ -1728,12 +1845,22 @@ window.__ModuleLoader__.load({
1728
1845
  loadShelfInventory,
1729
1846
  ...props
1730
1847
  });
1731
- const unregister = ctx.effect(() => configForms.whileServed([NS], () => ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
1732
- name: "settings.plugins.tab",
1733
- id: NS,
1734
- label: () => t("title"),
1735
- locale: NS
1736
- }, renderCard))), "dsh-hypatia-auto-memory: plugins settings tab");
1848
+ const unregister = ctx.effect(() => configForms.whileServed([NS], () => installPluginCard(ctx, {
1849
+ official: () => ctx.slots.register({
1850
+ name: BUNDLE_CONFIG_SEAT,
1851
+ key: BUNDLE_NAME,
1852
+ locale: NS
1853
+ }, renderCard),
1854
+ settingsTab: () => ctx.slots.register({
1855
+ name: SETTINGS_TAB_SEAT,
1856
+ id: NS,
1857
+ label: () => t("title"),
1858
+ locale: NS
1859
+ }, renderCard),
1860
+ onRefused: (seat, error) => {
1861
+ console.warn(`[${BUNDLE_NAME}] configuration card refused by slot "${seat}"`, error);
1862
+ }
1863
+ })), "dsh-hypatia-auto-memory: configuration card seat");
1737
1864
  const unregisterMemoryView = ctx.slots.inject("conversation.view", () => ctx.slots.register({
1738
1865
  name: "conversation.view",
1739
1866
  id: "memory",