dsh-hypatia-auto-memory 1.1.1 → 2.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
@@ -101,6 +101,8 @@ collect fiber dispose ──▶ the same flush for every session still open, per
101
101
  before the queue stops; the model call finishes at the next start
102
102
  (a restart, a config-change restart, or a crash: the startup
103
103
  consolidation backfill)
104
+ explicit request ──▶ /hypatia-summarize (no model turn) | hypatia_summarize |
105
+ the Memory tab button ──▶ the same consolidate task, thresholds waived
104
106
  agent/created ──▶ rules/taboos inject()
105
107
  ```
106
108
 
@@ -205,12 +207,50 @@ agent/created ──▶ rules/taboos inject()
205
207
  long. Cascade is the exception: it keeps thinking (it is compressing sixteen
206
208
  summaries, not filling a shape) and pays for it in the same allowance.
207
209
 
210
+ A route's `reasoningEffort` overrides all of that. Set it to a level the model
211
+ declares and every call on that route is sent that level — the allowance
212
+ follows, because anything other than `off` shares the cap with the answer. The
213
+ vocabulary is the adapter's own (the settings card offers exactly what the
214
+ model declares: `off`, `low`, `high`, `max` on `llm-deepseek`), and a value the
215
+ route does not declare is **dropped with one warning** rather than sent: the
216
+ core rejects an undeclared effort before any provider I/O, so sending it would
217
+ turn a profile typo into a task that retries and then fails permanently. Leave
218
+ the field out to keep the per-purpose policy above.
219
+
208
220
  - **Cascade** archives sixteen unarchived tier-N summaries into one tier-(N+1)
209
221
  entry, using hypatia's own `$not-summaried` (which anti-joins on
210
222
  `statement.tail` and orders `created_at ASC`, giving FIFO batching for free).
211
223
  This is the protocol's log₁₆(n) archive: each tier compresses only material a
212
224
  previous tier already distilled.
213
225
 
226
+ - **Consolidation can also be requested explicitly.** Three faces call one host
227
+ action: the slash command **`/hypatia-summarize`** (in the composer's `/` menu,
228
+ executed by the UI *without a model turn*), the **`hypatia_summarize`** tool
229
+ (the `hypatia-memory` skill's execution path, so the agent never hand-writes a
230
+ `sum-*` entry), and a **Summarise now** button on the Memory tab, which sits on
231
+ the row that reports how far behind the session is — the moment a reader wants
232
+ it. A request cuts the span `[lastConsolidatedSeq, min(seq, lastLoggedSeq))`
233
+ **at that moment** and queues the same `consolidate` task the automatic trigger
234
+ queues: same kind, same executor, same idempotent writer. `checkEveryTurns` and
235
+ `minNewTokens` are deliberately bypassed — they pace an ongoing conversation,
236
+ and an explicit request is the reason to cut now. Measured live, a session sat
237
+ 151 logged events behind while the token floor had accumulated 645 of 3000, so
238
+ waiting for the gate is not an answer to accept. The config switches are *not*
239
+ bypassed, and a request is only a cut, never a freeze: events appended
240
+ afterwards wait for the next trigger as usual.
241
+
242
+ "Nothing left to summarise" and "this span is already queued" are reported as
243
+ such rather than queued again, so asking twice does not produce two summaries —
244
+ and because the writer is idempotent by name, even a race cannot. The button is
245
+ **disabled but kept in place** once the session is caught up: hiding it would
246
+ make the user rediscover it every time the session falls behind, which is
247
+ exactly when they look for it. It rides the Memory tab's own route family,
248
+ whose first **write** route (`POST /consolidate`) keeps all three
249
+ self-authentication checks the read routes use — loopback socket, loopback
250
+ `Host`, same-origin — and refuses anything but `POST`, so the write cannot be
251
+ triggered by a link or an image tag. Nothing on any of the three faces runs a
252
+ model call in the main session.
253
+
214
254
  - **Work units** are adjudicated, not guessed. Candidates come from `similar`
215
255
  (the only search that reports a distance), with the operational layer left
216
256
  out — in the query, by `--exclude-tags`, on a binary that has it (hypatia
@@ -315,7 +355,7 @@ agent/created ──▶ rules/taboos inject()
315
355
  verbatim, the reader just wrote them, and the agent is the right reader for
316
356
  them.
317
357
 
318
- The data arrives over a session-scoped, read-only HTTP route this plugin
358
+ The data arrives over a session-scoped HTTP route family this plugin
319
359
  registers on the DSH web server, `/api/dsh-hypatia-auto-memory/session`: status
320
360
  from the in-memory state tables on every poll, the shelf content only when it
321
361
  can have changed (on open, on a session switch, on a manual refresh, and when
@@ -325,7 +365,10 @@ agent/created ──▶ rules/taboos inject()
325
365
  and 120 000 per response. JSE has no `ORDER BY`, so "newest" is decided only
326
366
  after reading every candidate; when a scan reaches its cap the counts become
327
367
  lower bounds, and the tab reports that response as truncated rather than
328
- leaving a short list unexplained.
368
+ leaving a short list unexplained. The family's one **write** route,
369
+ `POST /consolidate`, is what the **Summarise now** button calls; it writes
370
+ nothing itself — it queues the same consolidation task the automatic trigger
371
+ queues (see *On-demand* above).
329
372
  Two channels were rejected on the way here. A **session projection** would be
330
373
  the best shape, but the event that would drive it cannot be written from
331
374
  outside this repository: `Session.append` offers no way to set the envelope's
@@ -357,7 +400,26 @@ takes whichever seat exists and moves between them without ever appearing twice
357
400
  save, without a profile reload. Stored as the plugin entry's `config:` in the
358
401
  profile patch; a pre-0.1.7 `settings.yaml` section is imported once on first
359
402
  launch. All fields optional, defaults shown. A blank consolidation route is
360
- refused on save; a duplicate one is dropped at runtime with a warning:
403
+ refused on save; a duplicate one is dropped at runtime with a warning.
404
+
405
+ **`consolidation.models` is a priority order, not a rotation.** A task's first
406
+ attempt takes the first route, and each retry degrades to the next one, reusing
407
+ the last route past the end. Two consequences are worth knowing: the chain a
408
+ task can reach is `min(queue.maxAttempts, models.length)` long — with the
409
+ default `maxAttempts: 3` a fourth and later route is never used — and each
410
+ downgrade costs one `queue.retryDelayMs` (5 s), because it happens on a retry
411
+ rather than inside one attempt. This replaced a round-robin that used every
412
+ selected route regardless of preference; reorder the list, or use the card's
413
+ ↑/↓ buttons, to change which route is preferred.
414
+
415
+ **Each route may also set a thinking level.** `reasoningEffort` names an
416
+ adapter-owned level — the settings card offers exactly what the model declares,
417
+ which on `llm-deepseek` is `off`/`low`/`high`/`max`. Omitting it keeps the
418
+ per-purpose policy described under *Consolidator* above; setting it overrides
419
+ that policy for every call on that route, and the output cap grows to pay for
420
+ it. A level the model does not declare is dropped at call time with one warning
421
+ instead of being sent, because the core rejects an undeclared effort before any
422
+ provider I/O.
361
423
 
362
424
  ```yaml
363
425
  hypatia-auto-memory:
@@ -373,8 +435,12 @@ hypatia-auto-memory:
373
435
  toolLedger: false # off by default; true records a compact tool-call ledger
374
436
  consolidation:
375
437
  enabled: true
376
- # Each attempt, including a retry, rotates to the next selected route.
438
+ # PRIORITY order: every consolidation tries the first route, and a retry
439
+ # degrades to the next one (see the note above).
377
440
  models: [] # select one or more { provider, model } routes
441
+ # Each route may also carry `reasoningEffort: <level>` — a thinking level the
442
+ # card offers from what the model declares (off/low/high/max on deepseek).
443
+ # Omit it to keep the per-purpose policy.
378
444
  maxInputTokens: 16000 # transcript cap (chars/4 estimate)
379
445
  maxOutputTokens: 2000
380
446
  timeoutMs: 120000
@@ -389,8 +455,8 @@ hypatia-auto-memory:
389
455
  batchSize: 16 # entries per tier before archiving one tier up
390
456
  queue:
391
457
  concurrency: 1 # parallel sessions
392
- maxAttempts: 3
393
- retryDelayMs: 5000
458
+ maxAttempts: 3 # retries per task; also caps the model fallback chain
459
+ retryDelayMs: 5000 # and the delay before the next route is tried
394
460
  # NOT a batching knob. Spans are cut at turn/end; for a turn still running
395
461
  # this long, the next step/end flushes what is complete so far.
396
462
  flushWindowMs: 120000
@@ -474,14 +540,19 @@ now only projects plugin Config forms.)
474
540
  5. Which model a background call actually ran on is recorded per attempt in
475
541
  `~/.dsh/storages/hypatia_auto_memory_diag.json` (`tables.model_calls`,
476
542
  newest first by `seq`, bounded to 100). The settings list is not the answer:
477
- one process-local cursor rotates through `consolidation.models` across every
478
- span summary, adjudication and cascade call, so consecutive attempts
479
- alternate. `outcome` is `pending` (in flight, or the process died mid-call),
480
- `ok` (a usable result was produced), `incomplete` (the call settled without
481
- one — output cap, abort, unparseable reply) or `error` (the call threw); in
482
- the last two the cursor was consumed and nothing was stored. `detail` is a
483
- finish kind, a fixed label, or the provider's message, capped at 200
484
- characters — never message content and never model output.
543
+ `consolidation.models` is a priority order, and one task's rows walk it —
544
+ the first attempt names the head, a retry names the next route down, which
545
+ is how a fallback is confirmed to have happened. `effort` is what the call
546
+ actually SENT, which is not the configured value when the route refused it;
547
+ `outputTokens` / `reasoningTokens` are the route's own usage report, and they
548
+ are what says whether the cap went to the answer or to the reasoning (both 0
549
+ when the route reports no usage). `outcome` is `pending` (in flight, or the
550
+ process died mid-call), `ok` (a usable result was produced), `incomplete`
551
+ (the call settled without one — output cap, abort, unparseable reply) or
552
+ `error` (the call threw); in the last two the attempt was spent and nothing
553
+ was stored. `detail` is a finish kind, a fixed label, or the provider's
554
+ message, capped at 200 characters — never message content and never model
555
+ output.
485
556
  6. `hypatia backfill --status` reports the shelf's embedding debt. With a
486
557
  current hypatia, logging a turn adds nothing to it; only summaries and work
487
558
  units are pending until the next flush. `hypatia scope list --count` shows
@@ -500,7 +571,7 @@ now only projects plugin Config forms.)
500
571
  | Entries appear only after a turn finishes | By design: spans are cut at `turn/end`, or at the first `step/end` once a turn has run longer than `flushWindowMs`, so an entry never lacks its own tool results |
501
572
  | Logging works, no summaries | `consolidation.models` is empty or invalid — one warning at first trigger |
502
573
  | Summaries but no `sum2-*` | Fewer than `cascade.batchSize` unarchived tier-1 summaries in that project yet |
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 |
574
+ | 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 — a route whose `reasoningEffort` names a real level instead gets that 1024 plus the thinking allowance. 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 |
504
575
  | `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 |
505
576
  | 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 |
506
577
  | 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 |
@@ -512,7 +583,8 @@ now only projects plugin Config forms.)
512
583
  | Duplicate `msg-*` after weird manual edits | Delete the entry in hypatia and lower `lastLoggedSeq` for that session in the state domain — backfill recreates it once |
513
584
  | The agent's own `hypatia` write still asks for approval | `autoApprove: false`, the command pipes/redirects/chains outside quotes, or its first word is not one of `binaries` — only plain calls are answered, by design. Reads never reach approval at all, so nothing to fix there |
514
585
  | The agent got a memory protocol that tells it to log messages by hand | Another plugin registered `hypatia-memory` first (`dsh-hypatia` still in the profile), or a `hypatia-memory` exists on disk — typically `~/.agents/skills/hypatia-memory`, written by `hypatia skill install --agent codex`. Agent presets load disk skills in a layer nearer than plugins, so it wins in sessions even though the skill center may list this plugin. Remove the copy the startup warning names |
515
- | Can't tell which model actually ran | By design the choice rotates: one process-local cursor is shared by span summaries, adjudication and cascade, so consecutive calls alternate through `consolidation.models`. Each attempt lands in `~/.dsh/storages/hypatia_auto_memory_diag.json` (`model_calls`) with purpose, provider/model, outcome and duration. The usage ledger cannot answer this — it folds agent turns (`assistant/message`) and never sees a plugin's direct `llm.stream`. A `pending` row means the call is still in flight, or the process died mid-call: the row is written before the call and settled after it |
586
+ | Can't tell which model actually ran | The list is a priority order, so a task's first attempt names the head of `consolidation.models` and a retry names the route it degraded to. Each attempt lands in `~/.dsh/storages/hypatia_auto_memory_diag.json` (`model_calls`) with purpose, provider/model, the `effort` actually sent, outcome, duration and the route's own token split — reading one task's rows top-down is how a fallback is confirmed, and `effort` is how a configured thinking level is confirmed to have been honoured. The usage ledger cannot answer this — it folds agent turns (`assistant/message`) and never sees a plugin's direct `llm.stream`. A `pending` row means the call is still in flight, or the process died mid-call: the row is written before the call and settled after it |
587
+ | A configured `reasoningEffort` seems to do nothing | The route does not declare that level, so the call drops it rather than risk the core's `UNSUPPORTED_REASONING_EFFORT`. One warning names the route, the value and what the route does declare; `model_calls.effort` then reads empty for those attempts. The card offers exactly the levels the model advertises, so this normally only happens after editing the profile by hand or pointing the route at a different model |
516
588
  | Startup warnings never appear in the terminal | `dsh web` mounts no log exporter, so plugin log lines of any level go nowhere; the console exporter's default threshold would also drop warnings (warn is level 2, above info's 1). Mount a logger such as `dsh-logbook` or `dsh-boot-doctor` temporarily to read them |
517
589
  | Project scope looks wrong | Scope = basename of the session cwd's git top level (`git rev-parse --show-toplevel`), or of the cwd itself when git finds no work tree or cannot answer (not installed, a repo it refuses as unsafe). A `rev-parse` slower than 3 s leaves that one session on the cwd's own name. Two same-named checkouts share a scope by design; a linked worktree is scoped by its own directory, not the main checkout's; git resolves symlinks, so a checkout opened through a link named differently from its target gets the target's name. A name hypatia would rewrite is normalized first: a session at `/` is scoped `/`, commas become `_`, surrounding whitespace goes. Entries written before these fixes stay where they were stored: a subdirectory session's under that directory's name (`repo/src` wrote under `src` — the git root was never read), a session at `/` with no scope, `a,b` under both `a` and `b`, `foo,` under `foo` and global. Padded names were stored trimmed, which is what the query now asks for. Nothing migrates them; `knowledge-update --scopes` moves one entry and keeps its `created_at`. `hypatia scope list --count` (hypatia #30) shows every spelling in use and how many entries each holds |
518
590
 
@@ -611,7 +683,8 @@ dsh-hypatia-auto-memory/
611
683
  │ ├── cascade.js # log₁₆(n) hierarchical summary archive
612
684
  │ ├── model-log.js # bounded record of which model each attempt ran on
613
685
  │ ├── memory-status.js # per-session fold of the two state tables (pure)
614
- │ ├── memory-api.js # Memory tab + shelf listing: read-only route family
686
+ │ ├── memory-api.js # Memory tab + shelf listing; its one write route too
687
+ │ ├── on-demand.js # /hypatia-summarize + the hypatia_summarize tool
615
688
  │ ├── recall.js # rules/taboos preload at session start
616
689
  │ ├── auto-approve.js # approves the agent's own plain bash hypatia calls
617
690
  │ ├── skills.js # bundled skill registration (never shadows another provider)
package/README.zh-CN.md CHANGED
@@ -87,6 +87,8 @@ session/disposed ──▶ final flush + consolidation, thresholds waived (DSH
87
87
  collect fiber 拆除 ──▶ 对每个仍打开的会话做同样的收尾,并在队列停止前落盘;模型
88
88
  调用留到下次启动完成(重启、配置变更重启,或崩溃:启动时的
89
89
  consolidation backfill)
90
+ 显式请求 ──▶ /hypatia-summarize(不过模型)| hypatia_summarize | 记忆标签页按钮
91
+ ──▶ 同一个 consolidate 任务,但跳过阈值
90
92
  agent/created ──▶ rules/taboos inject()
91
93
  ```
92
94
 
@@ -163,11 +165,37 @@ agent/created ──▶ rules/taboos inject()
163
165
  正是工作单元让回复变长。Cascade 是例外:它保留 thinking(它是在压缩十六条摘要,不是
164
166
  在填一个形状),并用同一份余量来支付。
165
167
 
168
+ 路由上的 `reasoningEffort` 覆盖以上全部策略。把它设成该模型声明过的档位,这条路由上的
169
+ 每次调用都会带上它,预算余量也随之生效(任何非 `off` 的档位都与正文共用上限)。档位
170
+ 词表属于适配器自己(设置卡片只列出该模型声明的档位:`llm-deepseek` 上是
171
+ `off`/`low`/`high`/`max`);而**路由没有声明的值会被丢弃并记一条警告**,不会发出去——
172
+ 核心会在任何 provider I/O 之前拒绝未声明的档位,发出去等于把一处配置笔误变成反复重试
173
+ 后永久失败的任务。留空即保持上面的按用途策略。
174
+
166
175
  - **Cascade** 把十六个未归档的 tier-N 摘要归档为一条 tier-(N+1) 条目,用 hypatia 自带
167
176
  的 `$not-summaried`(它对 `statement.tail` 做反连接并按 `created_at ASC` 排序,
168
177
  免费得到 FIFO 分批)。这就是协议的 log₁₆(n) 归档:每一层只压缩上一层已经蒸馏过的
169
178
  内容。
170
179
 
180
+ - **也可以显式要求立刻整合。** 三个面对应同一个 host 侧动作:斜杠命令
181
+ **`/hypatia-summarize`**(在 composer 的 `/` 菜单里,由 UI 执行、**不过模型**)、工具
182
+ **`hypatia_summarize`**(`hypatia-memory` 技能的执行路径,Agent 因此不会自己手写
183
+ `sum-*` 条目),以及记忆标签页上的**「立即整理」按钮**——它就在报告「落后多少」的那
184
+ 一行上,正是读者需要它的时刻。一次请求按**触发那一刻**的水位切出
185
+ `[lastConsolidatedSeq, min(seq, lastLoggedSeq))`,入队与自动触发**完全相同**的
186
+ `consolidate` 任务:同一种 kind、同一个执行器、同一个幂等写手。`checkEveryTurns` 与
187
+ `minNewTokens` 被刻意绕过——它们用来给进行中的对话定节奏,而显式请求本身就是「现在
188
+ 就切」的理由。实测中一个会话落后 151 个事件,而门槛只累计到 3000 中的 645,所以
189
+ 「等闸门开」不是一个应当被接受的答案。配置开关**不**绕过;而且请求只是一次切分,
190
+ 不是冻结:之后产生的事件照旧等下一次触发。
191
+
192
+ 「没有需要整理的内容」和「这个区间已经入队」会如实回报而不是再入队一次,所以问两遍
193
+ 不会产生两条摘要——何况写手按名字幂等,即使并发也不会。结清后按钮**禁用但保留在原
194
+ 位**:隐藏它会让用户每次落后时都要重新发现它,而那正是他们找它的时候。它走记忆标签
195
+ 页自己的路由族,其中第一条**写**路由(`POST /consolidate`)完整保留读路由的三条自
196
+ 认证——回环 socket、回环 `Host`、同源——并且只接受 `POST`,所以链接或 `<img>` 无法触
197
+ 发这次写入。三个面都不会在主会话里发起模型调用。
198
+
171
199
  - **工作单元是裁决出来的,不是猜出来的。** 候选来自 `similar`(唯一会报告距离的
172
200
  检索),且排除操作层——在具备该能力的二进制上(hypatia #35)通过查询里的
173
201
  `--exclude-tags`;在没这个能力的二进制上,抓四倍的量再丢弃——再按距离上限过滤,
@@ -243,14 +271,16 @@ agent/created ──▶ rules/taboos inject()
243
271
  原始 `msg-*` 条目**故意不显示**:它是对话原文,读的人刚刚写过,而正确的读者是
244
272
  Agent。
245
273
 
246
- 数据经由本插件注册在 DSH web 服务器上的、会话作用域的只读 HTTP 路由
274
+ 数据经由本插件注册在 DSH web 服务器上的、会话作用域的 HTTP 路由族
247
275
  `/api/dsh-hypatia-auto-memory/session` 到达浏览器:状态每次轮询都从内存中的状态表
248
276
  现算,shelf 内容只在可能变化时才读(打开时、切换会话时、手动刷新时,以及整合推进了
249
277
  该会话水位时),其间缓存 15 秒。标签页在屏幕上时每 5 秒轮询一次,响应是有界的:
250
278
  最新的 40 条摘要与 40 条工作单元,每条正文 4000 字符、整份响应 120000 字符。
251
279
  JSE 没有 `ORDER BY`,所以「最新」只能在读完所有候选之后判定;扫描触到上限时计数
252
- 变成下界,标签页会把该次响应标为已截断,而不是让短列表无从解释。路上否掉了两条
253
- 通道。**session projection** 形态最
280
+ 变成下界,标签页会把该次响应标为已截断,而不是让短列表无从解释。这个路由族里唯一的
281
+ **写**路由 `POST /consolidate` 就是「立即整理」按钮调用的那个;它自己不写任何东西
282
+ ——它入队的是与自动触发完全相同的整合任务(见上文「也可以显式要求立刻整合」)。
283
+ 路上否掉了两条通道。**session projection** 形态最
254
284
  合适,但驱动它的事件在本仓库之外写不出来:`Session.append` 没有任何途径设置信封的
255
285
  `ignorable` 标记,而持久化读取路径会拒绝一个带着未知事件类型、又没有该标记的会话
256
286
  日志——所以自定义事件会破坏会话重载。**设置命名空间**即便在还存在时也是根作用域的:
@@ -273,7 +303,20 @@ agent/created ──▶ rules/taboos inject()
273
303
  下面每个字段保存即生效,无需重载 profile。配置以插件条目 `config:`
274
304
  的形式存进 profile patch;0.1.7 之前的 `settings.yaml` 段落会在首次启动时被一次性导入。
275
305
  所有字段可选,展示的是默认值。空白的巩固路由在保存时就被拒绝;重复的路由在运行时被丢弃
276
- 并记一条警告:
306
+ 并记一条警告。
307
+
308
+ **`consolidation.models` 是优先级顺序,不是轮换。** 一个任务的第一次尝试走第一条路由,
309
+ 之后每次重试降级到下一条,超出列表长度时复用最后一条。两个需要知道的后果:一个任务能触达
310
+ 的备选链长度是 `min(queue.maxAttempts, models.length)`——默认 `maxAttempts: 3` 时第四条
311
+ 及以后的路由永远不会被用到;而每降一级要等一个 `queue.retryDelayMs`(5 秒),因为降级发生
312
+ 在下一次重试,而不是同一次尝试内部。这是对原先「不管偏好、把所有已选路由轮流用完」的替换;
313
+ 改变偏好就重排列表,或直接用卡片上的 ↑/↓ 按钮。
314
+
315
+ **每条路由还可以设置思考程度。** `reasoningEffort` 取一个适配器私有的档位——设置卡片只
316
+ 列出该模型声明的那些,`llm-deepseek` 上是 `off`/`low`/`high`/`max`。留空即保持上文
317
+ *Consolidator* 里那套按用途策略;设了它就会覆盖该路由上所有调用的策略,输出上限也随之
318
+ 变大来支付这份思考。模型没有声明的档位会在调用时被丢弃并记一条警告,而不是发出去——核心
319
+ 会在任何 provider I/O 之前拒绝未声明的档位。
277
320
 
278
321
  ```yaml
279
322
  hypatia-auto-memory:
@@ -289,8 +332,10 @@ hypatia-auto-memory:
289
332
  toolLedger: false # 默认不写工具调用台账;true 才为有内容的助手消息记录
290
333
  consolidation:
291
334
  enabled: true
292
- # 每次尝试(含重试)轮换到下一条已选路由。
335
+ # 优先级顺序:每次整合先用第一条路由,重试才降级到下一条(见上文说明)。
293
336
  models: [] # 选择一条或多条 { provider, model } 路由
337
+ # 每条路由还可以带 `reasoningEffort: <档位>`——卡片只列出该模型声明的档位
338
+ # (deepseek 上是 off/low/high/max)。留空即保持按用途策略。
294
339
  maxInputTokens: 16000 # transcript 上限(chars/4 估算)
295
340
  maxOutputTokens: 2000
296
341
  timeoutMs: 120000
@@ -305,8 +350,8 @@ hypatia-auto-memory:
305
350
  batchSize: 16 # 归档上一层之前,每层的条目数
306
351
  queue:
307
352
  concurrency: 1 # 并行会话数
308
- maxAttempts: 3
309
- retryDelayMs: 5000
353
+ maxAttempts: 3 # 每个任务的重试次数;同时决定模型备选链能走多深
354
+ retryDelayMs: 5000 # 也是换到下一条路由前的等待时间
310
355
  # 不是分批旋钮。span 在 turn/end 切分;对一个跑这么久的 turn,
311
356
  # 下一个 step/end 会把已完成的部分刷出。
312
357
  flushWindowMs: 120000
@@ -374,12 +419,16 @@ shelf 清单本来就不是配置,而设置域现在只投影插件的 Config
374
419
  4. 检查路由告警:没选 `models` 的巩固只记录一次告警,其余时间静默。
375
420
  5. 后台调用实际用了哪个模型,逐次记在
376
421
  `~/.dsh/storages/hypatia_auto_memory_diag.json`(`tables.model_calls`,按 `seq`
377
- 倒序,上限 100 条)。设置卡片里的列表不是答案:一个进程内游标被 span 摘要、
378
- 裁决和 cascade 三者共用,所以相邻几次调用是轮流换模型的。`outcome` 为
379
- `pending`(在飞行中,或进程中途死了)、`ok`(产出了可用结果)、`incomplete`
380
- (调用结束了但没产出——撞输出上限、被中止、回复解析不出来)或 `error`(调用本身
381
- 抛错);后两种都消耗了游标且没有写入任何条目。`detail` 是 finish kind、固定标签
382
- 或 provider 的错误消息,截断到 200 字符——绝不放消息正文,也绝不放模型输出。
422
+ 倒序,上限 100 条)。设置卡片里的列表不是答案:`consolidation.models` 是优先级顺序,
423
+ 同一个任务的几行会沿它往下走——第一次尝试是队首那条,重试是降级后的下一条,这也正是
424
+ 确认「备选确实被用上了」的方式。`effort` 是这次调用**实际发出**的档位——路由拒绝了
425
+ 配置值时它就不是配置值;`outputTokens` / `reasoningTokens` 来自路由自己上报的用量,
426
+ 它们才说明输出上限是被正文吃掉的还是被推理吃掉的(路由不上报用量时两者都是 0)。
427
+ `outcome` 为 `pending`(在飞行中,或进程中途死了)、
428
+ `ok`(产出了可用结果)、`incomplete`(调用结束了但没产出——撞输出上限、被中止、回复
429
+ 解析不出来)或 `error`(调用本身抛错);后两种都花掉了一次尝试且没有写入任何条目。
430
+ `detail` 是 finish kind、固定标签或 provider 的错误消息,截断到 200 字符——绝不放消息
431
+ 正文,也绝不放模型输出。
383
432
  6. `hypatia backfill --status` 报告 shelf 的嵌入债务。用当前的 hypatia,记录一个
384
433
  turn 不会增加债务;只有摘要和工作单元在下一次 flush 前处于待处理。`hypatia scope
385
434
  list --count` 显示在用的 scope——本项目 `msg-*` 条目所带的那个,就是会话种子读取
@@ -397,7 +446,7 @@ shelf 清单本来就不是配置,而设置域现在只投影插件的 Config
397
446
  | 条目只在 turn 结束后才出现 | 设计如此:span 在 `turn/end` 切分,或当 turn 跑得比 `flushWindowMs` 久时在第一个 `step/end` 切分,所以条目不会缺它自己的工具结果 |
398
447
  | 有记录,没摘要 | `consolidation.models` 为空或无效——首次触发时告警一次 |
399
448
  | 有摘要但没有 `sum2-*` | 该项目里未归档的 tier-1 摘要还不足 `cascade.batchSize` 条 |
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>` 指向它已有的文件 |
449
+ | 工作单元没有关系 | shelf 没有嵌入模型(`similar` 失败),所有候选都超过了 `dedupMaxDistance`,或裁决调用本身没有产出裁决——`model_calls` 里表现为 `incomplete` / `max-tokens`,也就是开启了思考的路由把整个输出预算花在推理上、还没开始作答就撞了上限。现在裁决会先确认适配器声明了 `off`,再对该路由请求 `reasoningEffort: 'off'`;对无法接受它的路由则把上限提到 1024——而路由的 `reasoningEffort` 若指定了真实档位,拿到的就是这 1024 再加上思考余量。自 hypatia #19 起,本地模型在 `~/.hypatia/models/<org>/<name>`(或 Hugging Face 缓存)里找,不再挨着 shelf:一个以前能回答 `similar`、现在说 `is not installed` 的 shelf,需要 `hypatia model install <model>`,或 `hypatia model register <model> <dir>` 指向它已有的文件 |
401
450
  | `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` 边保留向量 |
402
451
  | 任务处于 `deferred` 状态 | live store 和 persistence 都没能提供它的会话。不是错误:它不消耗尝试次数,一旦二者之一能提供就运行。通常 storage 立即回答——只有组合里没有 `sessionPersistence`,或它的读取失败(日志里找 `could not be read`)时才会持久化这个状态。被 deferred 的巩固任务会在下次启动时被 backfill 重新入队,所以它不必等会话被重新打开 |
403
452
  | 任务处于 `failed` 状态 | 一次 hypatia 或模型错误在 `maxAttempts` 之后持久化了。失败的 `log-message` 记录在下次启动时被修剪,因为水位会重新推导它们的范围;其它类型保留供检查——删掉一条好让下一个触发器重新创建它 |
@@ -409,7 +458,8 @@ shelf 清单本来就不是配置,而设置域现在只投影插件的 Config
409
458
  | 手动乱改后出现重复 `msg-*` | 在 hypatia 里删掉该条目,并在 state domain 里把该会话的 `lastLoggedSeq` 调低——回填会重新创建它一次 |
410
459
  | Agent 自己的 `hypatia` 写入仍要审批 | `autoApprove: false`、命令在引号外有管道/重定向/串联、或首词不是 `binaries` 之一——按设计只回答纯调用。读取根本不会走到审批,所以那里没有要修的 |
411
460
  | Agent 拿到了一份教它手动记录消息的记忆协议 | 另一个插件先注册了 `hypatia-memory`(profile 里还有 `dsh-hypatia`),或磁盘上有 `hypatia-memory`——通常是 `hypatia skill install --agent codex` 写的 `~/.agents/skills/hypatia-memory`。Agent 预设会在比插件更近的一层加载磁盘技能,所以它在会话里胜出,尽管技能中心可能列出本插件。删掉启动告警指出的那份拷贝 |
412
- | 不知道实际用了哪个模型 | 设计上就是轮流的:span 摘要、裁决、cascade 共用一个进程内游标,所以相邻调用在 `consolidation.models` 里交替。每次尝试都落在 `~/.dsh/storages/hypatia_auto_memory_diag.json`(`model_calls`),带 purpose、provider/model、outcome 和耗时。usage ledger 答不了这个问题——它只折算 Agent 的 turn(`assistant/message`),看不见插件直连的 `llm.stream`。`pending` 行表示调用仍在进行中,或进程在调用中途死了:记录在调用前先落盘、调用结束后回填 |
461
+ | 不知道实际用了哪个模型 | 列表是优先级顺序:一个任务的第一次尝试就是 `consolidation.models` 的队首,重试则是它降级到的那条。每次尝试都落在 `~/.dsh/storages/hypatia_auto_memory_diag.json`(`model_calls`),带 purpose、provider/model、**实际发出的 `effort`**、outcome、耗时以及路由自己上报的 token 拆分——把同一个任务的几行按序读下来,就能确认备选确实被用上了,而 `effort` 是确认配置的思考档位真的生效了的依据。usage ledger 答不了这个问题——它只折算 Agent 的 turn(`assistant/message`),看不见插件直连的 `llm.stream`。`pending` 行表示调用仍在进行中,或进程在调用中途死了:记录在调用前先落盘、调用结束后回填 |
462
+ | 配了 `reasoningEffort` 却像没生效 | 该路由没有声明这个档位,于是调用把它丢掉了,以免撞上核心的 `UNSUPPORTED_REASONING_EFFORT`。会有一条警告点名路由、值和该路由实际声明的档位,此后这些尝试的 `model_calls.effort` 是空的。卡片只提供模型声明的档位,所以这通常只发生在手改 profile、或把这条路由换成了别的模型之后 |
413
463
  | 启动告警从不出现在终端 | `dsh web` 不挂日志导出器,所以插件任何级别的日志行都没地方去;控制台导出器的默认阈值也会丢掉警告(warn 是 2 级,高于 info 的 1 级)。临时挂一个 logger,如 `dsh-logbook` 或 `dsh-boot-doctor` 来读它们 |
414
464
  | 项目 scope 看起来不对 | Scope = 会话 cwd 的 git 顶层目录的 basename(`git rev-parse --show-toplevel`);git 找不到工作树或答不上(未安装、或某 repo 被判定不安全)时,则是 cwd 本身的 basename。一次慢于 3 秒的 `rev-parse` 会让那个会话留在 cwd 自己的名字上。两个同名 checkout 按设计共享 scope;linked worktree 按它自己的目录而非主 checkout 的目录定 scope;git 会解析符号链接,所以通过一个与目标不同名的链接打开的 checkout 会得到目标的名字。一个会被 hypatia 改写的名字会先归一化:位于 `/` 的会话定 scope 为 `/`,逗号变 `_`,去掉首尾空白。这些修复之前写的条目留在原处:子目录会话写在那个目录名下(`repo/src` 写在 `src` 下——git 根从未被读取)、位于 `/` 的会话无 scope、`a,b` 同时写在 `a` 和 `b` 下、`foo,` 写在 `foo` 和全局下。带空白的名字被去空白存储,这也正是查询现在所请求的。没有任何东西迁移它们;`knowledge-update --scopes` 可移动单条并保留其 `created_at`。`hypatia scope list --count`(hypatia #30)显示每种在用的拼写及其条目数 |
415
465
 
@@ -486,7 +536,8 @@ dsh-hypatia-auto-memory/
486
536
  │ ├── cascade.js # log₁₆(n) 分层摘要归档
487
537
  │ ├── model-log.js # 每次尝试实际用了哪个模型的有界记录
488
538
  │ ├── memory-status.js # 两张状态表按会话折叠(纯函数)
489
- │ ├── memory-api.js # 记忆标签页与 shelf 清单:只读路由族 + JSE 读取
539
+ │ ├── memory-api.js # 记忆标签页与 shelf 清单;含唯一的写路由
540
+ │ ├── on-demand.js # /hypatia-summarize 与 hypatia_summarize 工具
490
541
  │ ├── recall.js # 会话启动时的 rules/taboos 预载
491
542
  │ ├── auto-approve.js # 批准 Agent 自己的纯 bash hypatia 调用
492
543
  │ ├── skills.js # 随包技能注册(从不遮蔽其它提供者)