dsh-memu-memory 1.1.3 → 1.2.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/AGENTS.md +1 -1
- package/README.md +31 -2
- package/README.zh.md +30 -2
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +1 -1
- package/docs/emotion-model-spec.md +281 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +1 -1
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +33 -11
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +115 -0
- package/lib/cycle.js +40 -6
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +221 -19
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +335 -40
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +9 -1
- package/lib/state.js +29 -12
- package/lib/tools.js +49 -2
- package/lib/vector-profile.js +27 -0
- package/package.json +2 -1
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +212 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +320 -0
- package/tests/consolidation-fork.test.mjs +357 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +321 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +77 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +92 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +536 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +208 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +733 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +164 -0
- package/tests/windows-env.test.mjs +89 -0
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
1. **不改 DSH 源码**,只用插件、公开 service、RPC、slot;只用公开扩展点。
|
|
16
16
|
2. **不让主 agent 写全局记忆**:全局只由 curator 写,主 agent 只读。
|
|
17
|
-
3.
|
|
17
|
+
3. **记忆规则与动态记忆都走 plugin 消息**:当前版本不再注册 system prompt 段;静态 guide 在回合首步以 `<memory-guide rules="…">` 投放(`rulesKey` 处理切域/改稿/压缩丢失),动态记忆走独立 plugin 消息,保护 KV cache 并让注入可折叠。
|
|
18
18
|
4. **不每步注入**:普通回合首步注入;长工具回合只在出现尚未处理的真实人类消息批次时追加一次。工具日志、心跳不算人类消息。
|
|
19
19
|
5. **不自动删除记忆**:永久记忆移出 = 边缘化;删库必须显式确认;删会话默认只解绑。
|
|
20
20
|
6. **Python 不装进系统解释器 / 用户 site-packages**,只装 `engine/<version>/venv`,且 venv 里不留 pip(`--without-pip` 建,再用基础 Python 的 `pip --python` 指进去装)。
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ All screenshots are the real WebUI on isolated example data, not anyone's memori
|
|
|
59
59
|
- **Three layers, one timeline.** `short_term` drafts expire after 72 hours by default; `long_term` is the body of ordinary memory; `must_remember` needs a reason and a source, decays over 365 days with a 0.35 strength floor, and can be *marginalized* but never auto-deleted to make room.
|
|
60
60
|
- **Recall is a moment, not a dump.** The first turn after a session starts (or after compaction) gets an anchor block with budgets for pinned, core and global memory. After that, each user turn gets at most one recall. Deduplication and a token budget are enforced in code, not left to the prompt.
|
|
61
61
|
- **A date is either an anniversary or one day.** `MM-DD` recurs every year (a birthday); `YYYY-MM-DD` fires the day before and the day itself, then is spent. A memory that says "tomorrow" is stored as the dated form, so an appointment does not come back every year.
|
|
62
|
-
- **Memory fades unevenly.** Recency, access count, peak and end effects, primacy, habituation and flashbulb accuracy feed one strength score.
|
|
62
|
+
- **Memory fades unevenly.** Recency, access count, peak and end effects, primacy, habituation and flashbulb accuracy feed one strength score. Mood is described by four components (joy / anger / sorrow / fear, summing to 1) plus arousal (intensity) and dominance (sense of control); a compound mood such as jealousy is a named mix of the four, not a new axis. Mood only reorders recall: it never enters the injected text and never rewrites a memory body. Gating is the OR of three independent conditions — the dominant share is >= 0.35, |arousal - 0.35| >= 0.1, or |dominance| >= 0.25; when none holds, both multipliers are exactly 1. Older entries carry no emotion and keep the old valence path; the four axes are never inferred back from valence. A vivid memory can be more confident than it is accurate, and the UI says so.
|
|
63
63
|
- **Provenance is a fact, not a style.** Every entry carries `source` (`user_said` / `inferred` / `imagined` / `told`), `confidence`, `accuracy`, and disclosure rules. A vague recollection cannot impersonate a quote.
|
|
64
64
|
- **Contradictions wait.** A write that rewrites or contradicts an existing entry lands as `pending_review`: it is not injected, not recalled, and appears in a review queue until someone accepts or rejects it.
|
|
65
65
|
- **Secrets stay excluded.** `secret` visibility, group-chat channels, characters, worlds, branches and `knownBy` all constrain reads and edits. Automatic recall never injects a `secret` memory, and a `secret` is still readable by an explicit recall from the same folder — it hides a memory from the prompt, it is not a lock. What actually separates two conversations in one folder is the `context` scope (`channelId` / `characterId` / `worldId` / `branchId`); a blank scope on both sides means one shared scope, which is the point of a folder. The old `visibility: 'session'` level is gone — reading turns it into `shared` with its `context` untouched, and `node scripts/migrate-visibility.mjs` rewrites libraries that still store it (`--apply`, backs up first).
|
|
@@ -67,6 +67,34 @@ All screenshots are the real WebUI on isolated example data, not anyone's memori
|
|
|
67
67
|
- **Images are pointers and captions, not pretend vision.** Attachments are deduplicated by SHA-256, quota-limited and tiered; an image without a description is remembered as "received an image", never as a scene the model did not see.
|
|
68
68
|
- **Housekeeping happens in the background.** Consolidation runs in a seeded fork sub-session, never as an editable turn in your chat. Failed runs back off instead of reporting success.
|
|
69
69
|
|
|
70
|
+
### Active memory, measured
|
|
71
|
+
|
|
72
|
+
Idle consolidation is not "every quiet session gets a fork". Before a fork the plugin scores the evidence since the last cursor: human turns (`H`), content volume (`C`: human text, assistant text and deduped successful tool results, 2,000 characters per result) and effective tool work units (`U`), with `score = 0.35*min(H/3,2) + 0.45*min(C/4000,2) + 0.20*min(U/20,2)` (default threshold 0.8). A session idle for 15 minutes wakes below that score only when it is small; transcripts of 2,000+ events must earn their fork, because the host seeds the full prefix. A recent consolidation or a recent recall suppresses the idle fork.
|
|
73
|
+
|
|
74
|
+
More is not better: thin turns on a large transcript are skipped by the guard (about 37% fewer prompt tokens on the same scenarios, at the cost of roughly 1 in 22 labels arriving a little later), but once the unconsolidated range passes 600 events the check runs unconditionally, so decisions made early in a long session do not stay out of memory forever.
|
|
75
|
+
|
|
76
|
+
Four real long sessions from this machine were replayed read-only (no new chat) and their turns were counted:
|
|
77
|
+
|
|
78
|
+
| Session | Events | Turns | Score-qualified | Below threshold | Fresh human turns after cursor |
|
|
79
|
+
|---|---:|---:|---:|---:|---:|
|
|
80
|
+
| `62056bff…` | 5,913 | 12 | 6 | 6 | 0 |
|
|
81
|
+
| `adedb9ac…` | 5,766 | 23 | 16 | 7 | 0 |
|
|
82
|
+
| `e38a0de0…` | 85 | 2 | 0 | 2 | 0 |
|
|
83
|
+
| `edf7dc5c…` | 78 | 4 | 0 | 4 | 0 |
|
|
84
|
+
|
|
85
|
+
Below-threshold turns on the two small sessions can still wake through starvation; on the 2,000+ event sessions they do not. None of the four had new human input after its real cursor, so none would fork again immediately.
|
|
86
|
+
|
|
87
|
+
Provider-reported usage from real forks: one consolidation costs about **41k–217k input tokens** (including cache reads; it depends on memory-tool steps) and **0.8k–3.4k output**. At one 4-hour long session per day and 2–6 effective consolidations:
|
|
88
|
+
|
|
89
|
+
| Consolidations/day | Monthly input | Monthly output |
|
|
90
|
+
|---|---:|---:|
|
|
91
|
+
| 2 | ~5.6M | ~0.11M |
|
|
92
|
+
| 4 | ~11M | ~0.23M |
|
|
93
|
+
| 6 | ~17M | ~0.34M |
|
|
94
|
+
| 6 (worst measured fork each) | ~39M | ~0.7M |
|
|
95
|
+
|
|
96
|
+
For scale, the whole 4-hour session tree measured here consumed 132M input and 1.28M output in one day, so active-memory forks are about **0.2%–1%** of the total. These are token counts, not money; cache reads are usually billed at a lower rate. The exact figure depends on turn count, memory-tool steps and how often the session actually goes idle.
|
|
97
|
+
|
|
70
98
|
## One WebUI
|
|
71
99
|
|
|
72
100
|
The sidebar has exactly **one** entry: **MemU WebUI**. The memory wall and all settings live in the same navigation, with deep links such as `#/settings/memories`, `#/settings/folders` and `#/settings/stats`. The interface follows the browser locale (Chinese or English).
|
|
@@ -296,7 +324,8 @@ the idle window starts from real message activity or the agent's return to idle.
|
|
|
296
324
|
- **No cloud sync, no accounts, no cross-device sync.** This is a local plugin with no server-side identity.
|
|
297
325
|
- **Images without vision:** without an optional caption/vision endpoint, an image memory is a pointer plus what a person wrote about it. The plugin does not claim to see pixels.
|
|
298
326
|
- **Poisoned v4 sessions:** historical `format v4 message requires a producer-owned source kind` rows can be diagnosed but not rewritten; the plugin never edits private session files.
|
|
299
|
-
- **Agent presets:** a conversation whose preset (`i-am-yuike`, `yuike-standard`, …) carries its own `cordis.yml` composes its model-facing plugins from that list alone, and its scope never delivers `agent/pre-step` to this plugin. At runtime `lib/preset-scope.js` attaches
|
|
327
|
+
- **Agent presets:** a conversation whose preset (`i-am-yuike`, `yuike-standard`, …) carries its own `cordis.yml` composes its model-facing plugins from that list alone, and its scope never delivers `agent/pre-step` to this plugin. At runtime `lib/preset-scope.js` attaches an injection listener to that agent's own context, so **anchor and recall delivery works**. All conversations now receive the memory rules as one `<memory-guide rules="…">` plugin message on the first step of a turn; the plugin no longer registers a system-prompt section, and the message is replaced after a domain/prompt change or a compaction (`rulesKey` / `guideLost`). The **final tool list** is a third, separate question: it follows the preset's own plugin list and the host scope, and it cannot be inferred from whether this plugin is listed — observations in this repo record both "no `memu_*` tools" and `memu_private_*` present in some presets' tool chains. **Per-preset tool availability still needs a real-host check (W3)**; until then do not read this as "presets have no tools", nor as "listing the plugin is enough". The plugin never edits anyone's preset files.
|
|
328
|
+
- **Consolidation worker (C 包, 2026-10-30).** An idle or manual pass now works from a *fixed snapshot*: it takes the public `sessionQuery.observeSession()` lease (falling back to `readSession()` when the host offers no lease), cuts at the last complete `turn/end`, and drops unpaired tool calls and half steps — a live transcript whose last turn is still open is **deferred**, never consolidated. A job is settled only by a structured completion receipt that is cross-checked against the library index (an id the library does not contain is not a success, and a rewrite of an existing id counts as an update, not a new memory). Checked / pending / in-flight ranges and a lease are persisted separately in `consolidation.json`, so a crash, a timeout or a failed turn leaves the range pending instead of looking consolidated. **Limits:** the worker still runs inside the parent preset's own system prompt — it receives an explicit "you are a background consolidation worker, not this character" brief, which reduces but does not remove persona carry-over, because the host has no public API to replace that prompt; and when the conversation's tool surface carries no `memu_private_remember` / `memu_private_update` (or exposes memory only through PTC), consolidation reports `unavailable` (`worker-tools-missing` / `worker-tools-ptc-only`) rather than forking a worker that could not write. The parent's library, scope, domain and memory context are re-checked before dispatch, on every worker tool call, and again before any result is credited.
|
|
300
329
|
- **"Tomorrow" is resolved at write time.** A relative day (今天 / 明天 / 后天 / today / tomorrow) together with a month-day settles into one dated memory; a memory that only says "sometime next month" keeps no date rather than inventing one. A date that was already written as `MM-DD` is not rewritten — the fix applies to new writes.
|
|
301
330
|
- **Not in scope:** cloud MemU integration, source backfill for old entries, automatic repair of already-written v4 corruption.
|
|
302
331
|
|
package/README.zh.md
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
- **三层,一条时间线。** `short_term` 草稿默认 72 小时到期;`long_term` 是记忆主体;`must_remember` 需要理由与来源,按 365 天半衰并带 0.35 强度地板,可以**边缘化**,但不会为腾名额被自动删除。
|
|
60
60
|
- **召回是一个瞬间,不是一次倾倒。** 新会话或压缩后的第一个回合得到锚点块(pin / core / global 分段预算);之后每个用户回合最多一次召回。去重与 token 预算由代码执行,不交给提示词自觉。
|
|
61
61
|
- **日期要么是纪念日,要么是一天。** `MM-DD` 每年复发(生日);`YYYY-MM-DD` 在前一天和当天各响一次,之后就算用过了。说「明天」的记忆存成带年份的日期,不会变成每年都冒出来的周年。
|
|
62
|
-
- **记忆会不均匀地淡。**
|
|
62
|
+
- **记忆会不均匀地淡。** 新近、访问次数、峰终、首因、习惯化、闪光灯记忆的准确度共同参与强度。情绪用四项构成(joy / anger / sorrow / fear,和为 1)加唤醒度 `arousal`(强度)与掌控感 `dominance` 描述;「嫉妒」这类复合情绪是四项的具名混合比例,不是新维度。情绪只影响召回排序:不进注入文本,也不改记忆正文。门控是三条独立条件的 OR——主导项占比 ≥ 0.35、`|arousal − 0.35| ≥ 0.1`、`|dominance| ≥ 0.25`;三条都不满足时两项乘数恒为 1。旧条目没有 emotion 时继续走原来的 valence 路径,不从旧 valence 反推四轴。一条生动但可能记错的记忆会显示「细节未必准」,而不是假装确定。
|
|
63
63
|
- **来源是事实,不是文风。** 每条记忆带 `source`(`user_said` / `inferred` / `imagined` / `told`)、`confidence`、`accuracy` 与说出口径。模糊的推断不能冒充原话。
|
|
64
64
|
- **矛盾先挂起。** 与已有记忆同题改写或说法相反的新条进入 `pending_review`:不注入、不召回,只出现在复核队列,等人或整理作业裁决。
|
|
65
65
|
- **秘密不进自动召回。** `secret`、群聊频道、角色、世界线、剧情分支与 `knownBy` 共同约束读写;`secret` 永远不会被自动注入,但同文件夹里**主动检索仍然读得到**——它挡的是「自己冒出来」,不是保险箱。真正把同一文件夹里的两段对话隔开的是 `context` 作用域(`channelId` / `characterId` / `worldId` / `branchId`);两边都没填就是同一个作用域,这正是「文件夹」的含义。旧的 `visibility: 'session'` 级别已删除:读到就按 `shared` 处理、`context` 原样保留,仍存着该级别的库用 `node scripts/migrate-visibility.mjs` 重写(`--apply` 才写,先备份)。
|
|
@@ -67,6 +67,34 @@
|
|
|
67
67
|
- **图片是指针加描述,不是假装能看图。** 附件按 SHA-256 去重、有配额、分三级留存;没有描述时只记「收到一张图片」,绝不编造画面。
|
|
68
68
|
- **整理在后台。** 整理跑在种子 fork 子会话里,不会在你的待发区多出一轮;失败退避重试,不伪记成功。
|
|
69
69
|
|
|
70
|
+
### 主动记忆:实测触发与月开销
|
|
71
|
+
|
|
72
|
+
空闲整理不是“每个安静会话都 fork”。进 fork 前会先给上次游标之后的证据打分:真人回合 `H`、内容量 `C`(真人文字 + 助手正文 + 去重后的成功工具结果,单结果最多 2000 字符)、有效工具工作单元 `U`,`score = 0.35*min(H/3,2) + 0.45*min(C/4000,2) + 0.20*min(U/20,2)`,默认门槛 0.8。空闲 15 分钟时,只有小会话能凭饥饿直接唤醒;2000 条事件以上的 transcript 必须分数达标,因为宿主会种完整前缀。近期整理过或注入过记忆的会话不 fork。
|
|
73
|
+
|
|
74
|
+
整理不是越勤越好:大 transcript 的薄回合会被守卫跳过(同一批用例上约 −37% prompt token,代价是被跳过的那几个用例里约 1/22 条标签晚一点进记忆),但待整理区间超过 600 事件时无条件是检查一次,避免长会话早期的决定永远进不了记忆。
|
|
75
|
+
|
|
76
|
+
用本机 4 个真实长会话做只读回放(不开新对话),统计合格回合:
|
|
77
|
+
|
|
78
|
+
| 会话 | 事件数 | 回合数 | 分数触发 | 未达分数 | 游标后新真人回合 |
|
|
79
|
+
|---|---:|---:|---:|---:|---:|
|
|
80
|
+
| `62056bff…` | 5,913 | 12 | 6 | 6 | 0 |
|
|
81
|
+
| `adedb9ac…` | 5,766 | 23 | 16 | 7 | 0 |
|
|
82
|
+
| `e38a0de0…` | 85 | 2 | 0 | 2 | 0 |
|
|
83
|
+
| `edf7dc5c…` | 78 | 4 | 0 | 4 | 0 |
|
|
84
|
+
|
|
85
|
+
两条小会话的未达分数回合仍可能因饥饿唤醒;2000 条事件以上的大会话不会。四个会话在真实游标之后都没有新的真人输入,因此不会立即再 fork。
|
|
86
|
+
|
|
87
|
+
从真实 fork 的 provider usage 汇总:一次整理约 **4.1 万~21.7 万 input tokens**(含缓存读,取决于记忆工具步数)+ **0.8k~3.4k output**。按每天一次 4 小时长会话、每天有效整理 2~6 次:
|
|
88
|
+
|
|
89
|
+
| 每天整理次数 | 每月 input | 每月 output |
|
|
90
|
+
|---|---:|---:|
|
|
91
|
+
| 2 次 | ~5.6M | ~0.11M |
|
|
92
|
+
| 4 次 | ~11M | ~0.23M |
|
|
93
|
+
| 6 次 | ~17M | ~0.34M |
|
|
94
|
+
| 6 次(都按实测最贵的单次) | ~39M | ~0.7M |
|
|
95
|
+
|
|
96
|
+
作为参照,今天这棵 4 小时会话树实测 132M input + 1.28M output/日,主动记忆整理约占 **0.2%~1%**。以上是 token 量,不是金额;缓存读通常按更低单价计费。实际取决于回合数、记忆工具步数和会话真正空闲的次数。
|
|
97
|
+
|
|
70
98
|
## 一个 WebUI
|
|
71
99
|
|
|
72
100
|
侧边栏只留**一个**入口:**MemU WebUI**。记忆墙与全部设置在同一套导航内,支持深链,如 `#/settings/memories`、`#/settings/folders`、`#/settings/stats`。
|
|
@@ -288,7 +316,7 @@ return { content: originalContent, meta: { ...originalMeta, memoryInbound: batch
|
|
|
288
316
|
- **没有云同步、账号或跨设备同步。** 这是本地插件,没有服务端身份。
|
|
289
317
|
- **没有视觉时:** 图片记忆只是「指针 + 人写的描述」。插件不会声称看过像素。
|
|
290
318
|
- **v4 污染会话:** 历史 `format v4 message requires a producer-owned source kind` 行只能诊断,不能改写;插件不会编辑私有会话文件。
|
|
291
|
-
- **人格预设(agent preset):** 预设(`i-am-yuike`、`yuike-standard` 等)自带 `cordis.yml`,会话的模型可见插件集只由那份清单决定,它的 scope 也不会把 `agent/pre-step` 送到本插件。运行时 `lib/preset-scope.js`
|
|
319
|
+
- **人格预设(agent preset):** 预设(`i-am-yuike`、`yuike-standard` 等)自带 `cordis.yml`,会话的模型可见插件集只由那份清单决定,它的 scope 也不会把 `agent/pre-step` 送到本插件。运行时 `lib/preset-scope.js` 会把注入监听挂到该 agent 自己的 ctx 上,所以**锚点与召回的送达是能用的**。所有会话现在都通过回合首步的一条 `<memory-guide rules="…">` 插件消息接收记忆规则;插件不再注册 system prompt 段,规则在切域、改稿或压缩后由 `rulesKey`/`guideLost` 替换。**最终工具面**是第三件事,由该预设自己的清单与宿主 scope 决定——不能从「有没有列上本插件」推断,仓库里的观察既有「没有 `memu_*` 工具」,也有某些预设的工具链里存在 `memu_private_*`。**逐预设的真实工具名单仍需真机核实(W3)**;在那之前,别读成「预设都没有工具」,也别读成「列上插件就一定可用」。插件不会改任何人的预设文件。
|
|
292
320
|
- **相对日期在写入时结算:** 「今天 / 明天 / 后天 / today / tomorrow」加上一个月日落成带年份的日期;只说「下个月某天」的记忆不产生日期,也不替它编一个。已经按 `MM-DD` 写进去的旧条目不会被改写——这条只对新写入生效。
|
|
293
321
|
- **不在范围:** 云端 MemU 联调、旧条目的 source 回填、已写坏 v4 消息的自动修复。
|
|
294
322
|
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# 主动记忆 A/B 评测与快照凭证(F 包,provisional)
|
|
2
|
+
|
|
3
|
+
> **状态:provisional,不是金标。** 确定性 harness 不调用模型;§9 另有一轮真实模型 live 运行(工具调用协议),它回答的是「协议下产出可不可用、省多少 token」,不是主功率提升,也不是 gold。数据集里 14 个 case 仍全部是**合成代理标注**(`annotationSource: "synthetic-proxy"`,`review: "pending"`,`gold: false`),没有人工逐条确认。
|
|
4
|
+
>
|
|
5
|
+
> 实测日期 2026-10-30(Asia/Shanghai)。测试数字与摘要都是**带日期的快照**,改动 fixture 或规则后必须重跑并更新,不能当永久保证。
|
|
6
|
+
|
|
7
|
+
- 工作包:§13.9 F 包(回放与验收)+ §13.6 C 包的**接口侧**(快照身份、凭证、游标规则)
|
|
8
|
+
- 脚本:`scripts/active-memory-ab.mjs`(确定性回放,§2)+ `scripts/active-memory-ab-live.mjs`(真实模型,§9)
|
|
9
|
+
- 测试:`tests/active-memory-ab.test.mjs`(38 个用例,38 pass;含结构行回执→凭证 5 条)
|
|
10
|
+
- 数据集:`tests/fixtures/active-memory-ab/`(14 个 case,15 个文件;含 4 个规模梯度 case,见 §5.2)
|
|
11
|
+
- 确定性 harness 模型/网络调用:**0 次**;真实模型调用只在 `--live` 下发生,报告不落盘、不留模型原文(§9)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. 这一页回答什么
|
|
16
|
+
|
|
17
|
+
| 问题 | 本文的回答 | 不能从这里得出的结论 |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| 快照是不是固定的? | 是。每个 case 有 `throughSeq` + 快照 `sha256` pin,改动未重新钉会被拒绝加载 | 不代表快照里的对话是真实用户会话(它是合成的) |
|
|
20
|
+
| 三种条件(旧行为 / guide / guide+weighted)在调度上有什么差别? | 有确定结论:见 §5 | 不代表 guide 对模型行为有/没有效果(未测) |
|
|
21
|
+
| 检查结果能不能被凭证校验、游标会不会乱走? | 能,且失败/半步/不支持工具都不前移游标 | 不代表后台 worker 的语义判断质量(oracle 适配器给的是上界) |
|
|
22
|
+
| 覆盖率是多少? | 只给**区间**:oracle 上界 / no-value 下界 | 不能写成「覆盖率提升 X%」这种点估计 |
|
|
23
|
+
| 全局干扰有没有把私有写污染? | 泄漏 0,冲突进 held | 不代表真实全局库内容与真实干扰强度 |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. 运行方式
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 全量确定性回放,JSON 打到 stdout
|
|
31
|
+
node scripts/active-memory-ab.mjs
|
|
32
|
+
|
|
33
|
+
# 写机器可读报告(CRLF)
|
|
34
|
+
node scripts/active-memory-ab.mjs --out artifacts/active-memory-ab.json
|
|
35
|
+
|
|
36
|
+
# 只跑前 3 个 case / 只跑某一行
|
|
37
|
+
node scripts/active-memory-ab.mjs --cases 3
|
|
38
|
+
node scripts/active-memory-ab.mjs --row guide-weighted:oracle
|
|
39
|
+
|
|
40
|
+
# 人工重新钉 manifest 前先看当前摘要(不会自动改写 pin)
|
|
41
|
+
node scripts/active-memory-ab.mjs --digests
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
测试:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node --import ./tests/loader-register.mjs --test tests/active-memory-ab.test.mjs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**确定性脚本没有任何参数能开启真实模型调用**:`--real-model`、`--live`、`--model`、`--network`、`--gold` 一律直接报错退出。真实模型有独立的一支运行器(`scripts/active-memory-ab-live.mjs`),用法、两臂定义与实测结果见 §9;它同样不把 synthetic-proxy 数据集升级为 gold。
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. 数据集与固定快照
|
|
55
|
+
|
|
56
|
+
### 3.1 十四个 case
|
|
57
|
+
|
|
58
|
+
| case | 干扰维度 | 它在测什么 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `clean-dense-turn` | off | 密集回合:分数达标就检查,不等空闲 |
|
|
61
|
+
| `clean-thin-turn` | off | 薄回合:没有空闲窗口,也不该唤醒 |
|
|
62
|
+
| `clean-starved-small` | off | 小会话饥饿:空闲到阈值无条件检查一次 |
|
|
63
|
+
| `clean-large-thin` | off | 大 transcript(2090 事件)薄回合:不吃饥饿豁免 |
|
|
64
|
+
| `clean-half-step` | off | throughSeq 停在 `tool/call` 上:半步 → deferred,不推进游标 |
|
|
65
|
+
| `clean-duplicate-reread` | off | 主模型已写过一条 + 重复轮询:重复/已存在怎么算 |
|
|
66
|
+
| `clean-recent-recall` | off | 刚注入过记忆:抑制,而不是"检查过没东西" |
|
|
67
|
+
| `global-relevant` | relevant | 相关全局条目可以进上下文,但不许复制进私有写 |
|
|
68
|
+
| `global-irrelevant-conflict` | irrelevant-conflict | 无关且冲突的全局条目:只进 held,不进可召回覆盖 |
|
|
69
|
+
| `no-human-turn` | off | 纯工具轮询:`no-human-turn` 与"没有新事实"是两回事 |
|
|
70
|
+
| `clean-scale-100` | off | 规模梯度最小档(98 事件):低于阈值,饥饿豁免生效 |
|
|
71
|
+
| `clean-scale-1000` | off | 规模梯度(998 事件):仍在阈值下,行为与上一档一致 |
|
|
72
|
+
| `clean-scale-5000` | off | 规模梯度(4998 事件):越过阈值,薄回合不再吃饥饿豁免 |
|
|
73
|
+
| `clean-scale-20000` | off | 规模梯度上限区(19998 事件):验证阈值以上不再有豁免 |
|
|
74
|
+
|
|
75
|
+
### 3.2 pin(`tests/fixtures/active-memory-ab/manifest.json`)
|
|
76
|
+
|
|
77
|
+
快照摘要 = `sha256(canonicalJson({ schemaVersion, fromSeq, throughSeq, events[fromSeq..throughSeq] }))`。`throughSeq` 之后的事件是"尚未到达",**不进摘要、不进逻辑检查范围,也不进 worker seed**;实际 seed 仍是从 0 到当前 `throughSeq` 的完整连续前缀。
|
|
78
|
+
|
|
79
|
+
| case | throughSeq | 事件数 | 快照 sha256(前 12 位) |
|
|
80
|
+
|---|---:|---:|---|
|
|
81
|
+
| clean-dense-turn | 22 | 23 | `01b63a470444` |
|
|
82
|
+
| clean-thin-turn | 9 | 10 | `73cfdd46eb46` |
|
|
83
|
+
| clean-starved-small | 9 | 10 | `49145876868c` |
|
|
84
|
+
| clean-large-thin | 2089 | 2090 | `e828cde9d7ee` |
|
|
85
|
+
| clean-half-step | 3 | 4(+8 尾事件) | `c6eb87ef6767` |
|
|
86
|
+
| clean-duplicate-reread | 13 | 14 | `d48163e03141` |
|
|
87
|
+
| clean-recent-recall | 9 | 10 | `2ea689efcda4` |
|
|
88
|
+
| global-relevant | 4 | 5 | `0f4d97adf484` |
|
|
89
|
+
| global-irrelevant-conflict | 9 | 10 | `0a1d5e3a7396` |
|
|
90
|
+
| no-human-turn | 5 | 6 | `851fa7965cbe` |
|
|
91
|
+
| clean-scale-100 | 97 | 98 | `ec6180fcd444` |
|
|
92
|
+
| clean-scale-1000 | 997 | 998 | `04510b40d08e` |
|
|
93
|
+
| clean-scale-5000 | 4997 | 4998 | `dc9095796c6c` |
|
|
94
|
+
| clean-scale-20000 | 19997 | 19998 | `400e916841df` |
|
|
95
|
+
|
|
96
|
+
数据集级 `casesDigest`(所有 pin 的 sha256):`8f44dfa8e4ab7818a1e750e9f2e26c8b71e97e00e3cb5967294a3f29ef9c0d75`(14 个 case 版本;加入规模梯度前的 10-case 版本是 `93e1f663…`)。
|
|
97
|
+
|
|
98
|
+
大 transcript 那个 case 用确定性填充(`ab-polling-padding-v1`):520 组完全相同的轮询结果,展开成 2080 个事件后参与摘要,因此既可复现又能跨过 2000 事件阈值;这些结果去重后只算 **1 个工作单元**(测试里有断言)。
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 4. 三组条件与四行
|
|
103
|
+
|
|
104
|
+
| 行 | 条件 | 检查时机 | 逻辑检查范围 / 实际 seed | 有无 guide |
|
|
105
|
+
|---|---|---|---|---|
|
|
106
|
+
| `legacy` | 旧行为 | 只有空闲 ≥ 3 分钟 | 整段前缀;实际 seed 为完整连续前缀 `[0..boundary]` | 无 |
|
|
107
|
+
| `guide` | 统一 guide | 同 legacy(guide 只改投放的规则消息) | 整段前缀;实际 seed 为完整连续前缀 `[0..boundary]` | 有(`rulesKey` 占位符) |
|
|
108
|
+
| `guide-weighted:oracle` | guide + 加权检查 + 快照 worker | `assessWake`(分数/饥饿/大 transcript)+ 抑制 | 逻辑资格范围是游标之后的范围;实际 seed 仍为完整连续前缀 `[0..boundary]` | 有 |
|
|
109
|
+
| `guide-weighted:no-value` | 同上,但 worker 总返回 `no_value` | 同上 | 同上;**没有 windowed seed** | 有 |
|
|
110
|
+
|
|
111
|
+
后两行是**同一调度、不同适配器**:oracle 给覆盖上界,no-value 给下界。真实模型落在两者之间,本 harness 不测。
|
|
112
|
+
|
|
113
|
+
触发计划全部由生产纯函数复算(`lib/active-memory.js` 的 `collectEvidence / scoreEvidence / assessWake / suppressionReason`),没有另写一套阈值;旧行为那 3 分钟空闲常量在脚本里是 `LEGACY_IDLE_MINUTES`,只用于对照。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 5. 实测结果(2026-10-30,合成代理集,interference = off 的 12 个 case)
|
|
118
|
+
|
|
119
|
+
| 指标 | legacy | guide | guide-weighted:oracle | guide-weighted:no-value |
|
|
120
|
+
|---|---:|---:|---:|---:|
|
|
121
|
+
| 覆盖 | 14/20(main 1 / 后台 13) | 14/20(同左) | **11/20**(main 1 / 后台 10) | 1/20(只有 main 1) |
|
|
122
|
+
| 未覆盖 | 6 | 6 | 9 | 19 |
|
|
123
|
+
| 延迟(人类回合)mean/median/max | 0.643 / 1 / 1 | 0.643 / 1 / 1 | 1.182 / 1 / 3 | 0 / 0 / 0(n=1) |
|
|
124
|
+
| no_value 率 | 1/9 = 0.111 | 1/9 = 0.111 | 0/6 = 0 | 5/6 = 0.833 |
|
|
125
|
+
| 重复提议 / 已存在 | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 |
|
|
126
|
+
| 后台检查次数 | 9 | 9 | 6 | 6 |
|
|
127
|
+
| 每千步骤检查次数 | 1.280 | 1.280 | 0.853 | 0.853 |
|
|
128
|
+
| 种子 token 估算(总/每次) | 46019 / 5113.2 | 46019 / 5113.2 | **4240 / 706.7** | 4240 / 706.7 |
|
|
129
|
+
| 父会话等待(未测) | 未测(null) | 未测(null) | 未测(null) | 未测(null) |
|
|
130
|
+
| 被拦原因分布 | `no-idle-gap` × 12 | 同左 | `low-score` × 10, `large-transcript-thin` × 3, `recent-recall` × 2, `no-human-turn` × 1 | 同左 |
|
|
131
|
+
| 唤醒触发 | `legacy-idle` × 9 | 同左 | `score` × 1, `starved` × 5 | 同左 |
|
|
132
|
+
| deferred / failed / 凭证不合法 | 0 / 0 / 0 | 0 / 0 / 0 | 1 / 0 / 0 | 1 / 0 / 0 |
|
|
133
|
+
|
|
134
|
+
标签总数 20,来自 12 个 interference = off 的 case(`global-relevant` / `global-irrelevant-conflict` 不进主表)。
|
|
135
|
+
|
|
136
|
+
相对 legacy 的差(同一批 case):
|
|
137
|
+
|
|
138
|
+
| 行 | 覆盖差 | 未覆盖差 | 检查次数差 | 种子 token 差 |
|
|
139
|
+
|---|---:|---:|---:|---:|
|
|
140
|
+
| guide | 0 | 0 | 0 | 0 |
|
|
141
|
+
| guide-weighted:oracle | −3 | +3 | −3 | −41779(约 −91%;检查次数/边界不同;每次 seed 仍是完整前缀) |
|
|
142
|
+
| guide-weighted:no-value | −13 | +13 | −3 | −41779(检查次数/边界不同;每次 seed 仍是完整前缀) |
|
|
143
|
+
|
|
144
|
+
**覆盖反转**:`guide-weighted:oracle` 现在是 11/20,**低于** legacy 的 14/20。原先那张表里加权比 legacy 多 1 条,那个结论不再成立。逐 case 拆开是:
|
|
145
|
+
|
|
146
|
+
| case | legacy | 加权 oracle | 差 | 原因 |
|
|
147
|
+
|---|---:|---:|---:|---|
|
|
148
|
+
| `clean-dense-turn` | 0/4 | 4/4 | +4 | 分数达标就检查,不等空闲 |
|
|
149
|
+
| `clean-large-thin` | 2/2 | 0/2 | −2 | `large-transcript-thin` 拦停 |
|
|
150
|
+
| `clean-scale-5000` | 2/2 | 0/2 | −2 | 同上(新增档) |
|
|
151
|
+
| `clean-scale-20000` | 2/2 | 0/2 | −2 | 同上(新增档) |
|
|
152
|
+
| `clean-recent-recall` | 1/1 | 0/1 | −1 | `recent-recall` 抑制 |
|
|
153
|
+
|
|
154
|
+
净 −3。原先「加权覆盖更好」只在 8-case 混比下成立,是 fixture 规模的产物,不是规则属性:那批 case 里只有 1 个超过 2000 事件阈值,而今天有 3 个。规则实际在做的事是拿覆盖换 seed 成本,换多少取决于 transcript 规模分布,不取决于规则本身。
|
|
155
|
+
|
|
156
|
+
### 5.1 全局干扰(独立维度,只取 guide-weighted:oracle 行)
|
|
157
|
+
|
|
158
|
+
| 干扰等级 | case 数 | 后台检查 | 锚点 token 估算 | 泄漏 | held | 覆盖/未覆盖 |
|
|
159
|
+
|---|---:|---:|---:|---:|---:|---|
|
|
160
|
+
| off | 12 | 6 | 0 | 0 | 0 | 11 / 9 |
|
|
161
|
+
| relevant | 1 | 1 | 82 | 0 | 0 | 1 / 0 |
|
|
162
|
+
| irrelevant-conflict | 1 | 1 | 41 | 0 | 1 | 1 / 0 |
|
|
163
|
+
|
|
164
|
+
- 主表**只**用 interference = off 的 case 计算(测试里有断言),所以"全局干扰"不会被算进主调度效果,也不会替主调度背锅。
|
|
165
|
+
- 泄漏检查是"全局 id 出现在 `createdIds`/`updatedIds`/`heldIds`",三行都是 0。
|
|
166
|
+
- 冲突 case 的 `outcome` 是 `mixed`:一条正常写入 + 一条进 `heldIds`,held 不算可召回覆盖。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### 5.2 规模梯度:阈值切换点与它的代价
|
|
171
|
+
|
|
172
|
+
四个规模 case 与 `clean-large-thin` 同源:标签、证据、分数逐项相同,唯一变量是 `padding.count`(98 / 998 / 4998 / 19998 事件)。数字直接取自报告 `cases[]`,按 `caseId` + `rowId` 取。
|
|
173
|
+
|
|
174
|
+
| case | 事件数 | legacy 检查 | legacy 覆盖 | legacy 种子 token | oracle 检查 | oracle 覆盖 | oracle 种子 token |
|
|
175
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
176
|
+
| `clean-scale-100` | 98 | 1 | 2/2 | 176 | 1 | 2/2 | 176 |
|
|
177
|
+
| `clean-scale-1000` | 998 | 1 | 2/2 | 1632 | 1 | 2/2 | 1632 |
|
|
178
|
+
| `clean-scale-5000` | 4998 | 1 | 2/2 | 8103 | 0 | 0/2 | 0 |
|
|
179
|
+
| `clean-scale-20000` | 19998 | 1 | 2/2 | 32368 | 0 | 0/2 | 0 |
|
|
180
|
+
|
|
181
|
+
- **切换点正好落在 2000。** 998 事件仍然检查(`starved`,1 次 completed);4998 事件完全不检查(0 次)。两档的 `evidence` 一致(humanTurns 2、toolUnits 1、score 0.249),legacy 也各检查 1 次,所以差异只能来自 `transcriptEvents > 2000`。2090 事件的 `clean-large-thin` 同样在加权行被拦成 0 次,与这条阈值一致。
|
|
182
|
+
- **守卫的代价可以量化。** 19,998 事件下一次 fork 的种子 token 估算 32,368(约 3.2 万),守卫把它降到 0;同一 case 的标签覆盖同时从 2/2 掉到 0/2。2090 事件档同理:3399 → 0,2/2 → 0/2。省下的是整个 fork,不是更便宜的 fork。
|
|
183
|
+
- **每次 seed 成本随事件数近似线性。** 四档折算每事件 176/98 = 1.80、1632/998 = 1.64、8103/4998 = 1.62、32368/19998 = 1.62 token。98 → 19998 多出的 19,900 个事件多花 32,192 token,边际 1.62 token/事件;98 档偏高是小档固定开销(约 17 token)。
|
|
184
|
+
- **边界。** 这证明的是**触发规则**在阈值上下的差异,不证明漏记问题被解决;种子仍是完整连续前缀 `[0..throughSeq]`,不是 windowed seed。守卫把 seed 变成 0,不是因为 seed 变短了。
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. 指标定义(照抄,避免下次改口径)
|
|
189
|
+
|
|
190
|
+
| 指标 | 定义 | 口径警告 |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| 覆盖 coverage | 有标签的 id 里,被"主模型主动写"或"后台写"覆盖的比例;按 `mainProactive / background / held / uncovered` 分开计 | oracle 适配器给上界,不是模型实测 |
|
|
193
|
+
| 延迟 latency | 标签的 `sourceSeq` 到首次写入边界之间的**人类回合数**与 **step/end(已完成步骤)数** | 不是毫秒;本 harness 不测真实 prompt 延迟 |
|
|
194
|
+
| no_value 率 | `status = completed` 且 `outcome = no_value` 的比例;分母是所有已执行的 checks(含 deferred) | 只有 completed/no_value 进分子;抑制不产生 check,`skipped`、`failed`、`deferred` 仍留在 checks 分母 |
|
|
195
|
+
| 重复 duplicates | 已写过的 id 再次出现在检查窗口里(`reproposed`)与重复 update(`alreadyPresent`) | 重复写不算新增覆盖 |
|
|
196
|
+
| held | `heldIds` 条数;与可召回覆盖分开 | held ≠ 覆盖,也 ≠ no_value |
|
|
197
|
+
| 调用 calls | 后台检查次数、每千 step/end 的检查次数、主模型记忆工具调用数 | 调用数不等于写入成功数 |
|
|
198
|
+
| 证据 evidence | 窗口内真人回合数(以及 `collectEvidence` 的字符/工作单元分项) | 轮询重复只算一次 |
|
|
199
|
+
| 成本 cost | `lib/budget.js estTokens` 对每次检查实际送进 worker 的完整连续前缀 `[0..throughSeq]` 事件文本求和;总量只累加实际触发的 checks | `estimateOnly: true`,不是实际 token 用量;总量差异可能只来自检查次数/边界,不是 windowed seed |
|
|
200
|
+
| 阻塞 blocking | 主会话等待没有持续时间观测,报告固定为 `mainSessionWaitForCheck: null` / unknown | 不是实测的阻塞耗时;不能写成 0 |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 7. 怎么读这些数字(重要)
|
|
205
|
+
|
|
206
|
+
1. **oracle 上界与 no-value 下界之间的差,是"模型有没有找到应记事实"的空间,不是本 harness 的结果。** 真实模型落在中间某处,本 harness 不给点估计。
|
|
207
|
+
2. **guide 行与 legacy 行完全相同**:在这批 fixture 上,guide 只改变投放的规则消息,不改变什么时候检查。guide 对模型遵从性的影响只能由真实模型调用测出;本文不作结论。
|
|
208
|
+
3. **加权这一行的总估算较低,不代表每次 seed 变短**:检查次数 −3、总种子 token −41779(约 −91%),因为触发的检查次数/边界不同;每次实际 seed 仍是完整连续前缀。父会话阻塞与等待时长均未测,报告为 unknown / `null`,因此不能推导阻塞改善或等待差异;覆盖净 −3 是两股力量对冲的结果:`clean-dense-turn` 的 +4 来自"密集回合也能检查",三个越阈值 case 的 −6 与 `recent-recall` 的 −1 来自守卫和抑制,不是来自更好的语义判断。
|
|
209
|
+
4. **no-value 下界说明"更省"不等于"更好"**:同调度、worker 总是空返回时覆盖从 6 掉到 1。两边必须一起看。
|
|
210
|
+
5. **大 transcript 那个 case 是这一轮的核心里程碑**:旧路径在薄回合上仍 fork(1 次,完整前缀 seed 3399 token 估算),加权路径按 `large-transcript-thin` 拦住(0 次)。这只证明**触发规则**生效,不证明漏记问题被解决;它不是 windowed-seed 的收益。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 8. 人审候选与 gold gating 是两件事
|
|
215
|
+
|
|
216
|
+
- 候选:case 存在、标签写了 `sourceSeq`、有人读过 → 仍只是 `review: "pending"`。
|
|
217
|
+
- 金标:需要人工逐条确认后把 `review` 改成 `human-reviewed`。**本 harness 不改它**,报告里 `goldGating.reviewedCases` 恒为设备实际值(今天是 0),`goldGating.goldAllowed === false`。
|
|
218
|
+
- 结构校验强制这条纪律:`annotationSource: "human"` 但 `review` 不是 `human-reviewed` → 直接 fixture 错误;任何 `gold: true` 的合成代理 case → 直接 fixture 错误。
|
|
219
|
+
- 报告的 `status` 恒为 `synthetic-proxy-not-gold`,`gold` 恒为 `false`。要改这两项只能改数据集的机器可读状态,而那是人审之后的事。
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 9. 真实模型 A/B:工具调用协议下的双臂实测(2026-10-30)
|
|
224
|
+
|
|
225
|
+
`scripts/active-memory-ab-live.mjs` 用工具调用协议跑真实模型:模型 `deepseek-flash`,OpenAI 兼容接口 `https://api.deepseek.com`。旧的 JSON 协议运行器已随 1.2.0 删除:它测的是无工具环境的降级路径,75 次请求全部 `candidates-invalid`,不产生任何可评分信号。
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# 只看计划,不联网
|
|
229
|
+
node --import ./tests/loader-register.mjs scripts/active-memory-ab-live.mjs --dry-run
|
|
230
|
+
# 真跑(key 只从环境变量读)
|
|
231
|
+
ACTIVE_MEMORY_AB_API_KEY=... node --import ./tests/loader-register.mjs scripts/active-memory-ab-live.mjs --live --arm both --out report.json
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
两臂只差一个变量:**baseline** 每个 case 都送模型;**weighted** 只送生产调度器判定要检查的 case(`assessWake` + 抑制),其余记 `scheduled-out`。提示词(system-guide + private-consolidation + 本地工具契约 + 任务书)、工具表、转录在两臂之间完全相同。半步(`tool/call` 无配对结果)由硬守卫挡住,两臂都不发请求,所以 baseline 是 13 次调用而不是 14 次。
|
|
235
|
+
|
|
236
|
+
同一配置连跑两遍(报告是仓库外的临时 JSON,不提交):
|
|
237
|
+
|
|
238
|
+
| 指标 | baseline | weighted |
|
|
239
|
+
|---|---:|---:|
|
|
240
|
+
| 实际调用 | 13 | 8(5 个 scheduled-out)|
|
|
241
|
+
| 参与统计的标签 | 22 | 15 |
|
|
242
|
+
| 覆盖(第 1 / 第 2 遍) | 11 / 9 | 7 / 9 |
|
|
243
|
+
| 覆盖率 | 0.50 / 0.41 | 0.47 / 0.60 |
|
|
244
|
+
| 写出的记忆条数 | 11 / 9 | 7 / 9 |
|
|
245
|
+
| **precision(写出即命中)** | **1.00 / 1.00** | **1.00 / 1.00** |
|
|
246
|
+
| prompt tokens | 36,392 | 23,107(−37%)|
|
|
247
|
+
| 填了 emotion 的条数 | 3 / 3 | 4 / 5 |
|
|
248
|
+
| 填了 dominance 的条数 | 0 / 0 | 0 / 0 |
|
|
249
|
+
|
|
250
|
+
**协议对了,产出就可用了。** 同一批 case,JSON 协议下 0 条可解析;工具调用下每一次调用都写出了合法记忆,且**写出即命中**(precision 1.00,两遍都是)——没有一条是凭空编的。此前 0.77–0.80 的 precision 是漏了整理提示词的产物,不是模型行为。
|
|
251
|
+
|
|
252
|
+
**守卫的账可以算清。** 把两臂对齐到同 8 个 case,覆盖是 10 对 8(标签基数 15),差异落在下面的方差内;被跳过的 5 个 case 共 7 条标签,baseline 在那里平均只覆盖 1 条。也就是说:**−37% prompt token 换约 1/22 条标签的延迟**,不是覆盖的永久损失——游标不前移,内容仍在 pending。
|
|
253
|
+
|
|
254
|
+
**情绪只在人物侧生效。** 工程类 fixture 上 emotion 只填在涉及人的条目(3–5 条),dominance 全为 0——这正是提示词要求的:工程事实固定中性,dominance 只跟人物侧 emotion 一起填。不要把这读成「模型没填」。
|
|
255
|
+
|
|
256
|
+
边界:
|
|
257
|
+
|
|
258
|
+
- 覆盖是**词面匹配**(overlap ≥ 0.3 或命中拉丁/数字强标记),不是语义判断,也不是人工金标;
|
|
259
|
+
- 标签仍是 `synthetic-proxy`,0 条人审;
|
|
260
|
+
- **n = 2**,两遍差异可达 ±2 条,任何小于这个幅度的改动都不可分辨;
|
|
261
|
+
- 与生产仍有唯一一处差异:生产把转录作为 fork 的种子会话传入,运行器把转录渲染成带 seq 的文本附在消息里。
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 10. C 包接口(可注入适配器)
|
|
266
|
+
|
|
267
|
+
F 包与 C 包并行开发,接口先按契约落地,C 的真实现只要满足这两条就能接进来,不需要改 harness:
|
|
268
|
+
|
|
269
|
+
### 10.1 适配器输入 / 输出
|
|
270
|
+
|
|
271
|
+
适配器是 `async (ctx) => credentialPayload`,通过 `runAb({ workers: { 'no-value': fn } })` 注入(`rowIds` 可只选一行)。
|
|
272
|
+
|
|
273
|
+
`ctx`:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
caseId, rowId, condition, path ('full-prefix-fork' | 'snapshot-worker')
|
|
277
|
+
fromSeq, throughSeq, snapshotSha256, scopeGeneration // 固定快照身份
|
|
278
|
+
events // 逻辑检查范围 [fromSeq, throughSeq]
|
|
279
|
+
seedEvents // 实际 worker seed:完整连续前缀 [0, throughSeq]
|
|
280
|
+
labels, labelsInRange, existingIds // 已有 id 不能再算新增
|
|
281
|
+
interferenceInput { level, globalIds, conflictLabelIds, anchorTokens }
|
|
282
|
+
halfStep, now, transcriptEvents, evidence
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
返回(**边界身份必须由 harness 填,适配器不许自报**):
|
|
286
|
+
|
|
287
|
+
```text
|
|
288
|
+
status: completed | skipped | deferred | failed
|
|
289
|
+
outcome: created | updated | mixed | no_value
|
|
290
|
+
createdIds / updatedIds / heldIds / reasonCode / retryable
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 10.2 凭证与游标规则(`validateCredential` / `canAdvance`)
|
|
294
|
+
|
|
295
|
+
- `checked.{fromSeq, throughSeq, snapshotSha256, scopeGeneration}` 必须与请求逐项相同,否则凭证不合法;
|
|
296
|
+
- 同一 id 不能出现在两个桶里;非 `completed` 不得声称写入了任何 id;`no_value` 不得带 created/updated;
|
|
297
|
+
- **只有** `completed`(含 `no_value`)前移游标;`deferred / skipped / failed / 凭证不合法` 一律不动游标;半步(`tool/call` 没有配对 `tool/result`)由 harness 直接判 `deferred: half-step`,worker 不参与;
|
|
298
|
+
- 相邻检查的 `fromSeq` 必须等于上一段是否推进后的游标,报告 `structureChecks` 为空才算 `ok: true`。
|
|
299
|
+
|
|
300
|
+
### 10.3 与 C 实现的边界
|
|
301
|
+
|
|
302
|
+
- 本 harness **不** fork、**不**写库、**不**读 `state.json`、不创建任何用户目录(测试里用 sentinel 断言);§9 的 live 运行器遵守同一组边界,且不留模型原文、不留转录正文、不落 key;
|
|
303
|
+
- C 的真实 worker(专用整理职责、真实工具面、真实 CAS)接进来时,只要按 §10.1 返回,就能拿到同一批指标;
|
|
304
|
+
- 现在这两行适配器是**注解预言机**与**空返回**,它们量的是"管线的上/下界",不是 worker 质量;适配器收到的 `seedEvents` 也遵循生产的完整前缀语义。
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### 10.4 与 C 包实现的字段对齐(并行开发,只读核对)
|
|
309
|
+
|
|
310
|
+
C 包的 `lib/consolidation-receipt.js` 与 `lib/consolidation-snapshot.js` 用同一套枚举与同一段覆盖范围,可以直接接进来:
|
|
311
|
+
|
|
312
|
+
| 本文 adapter payload | C 包 receipt | 说明 |
|
|
313
|
+
|---|---|---|
|
|
314
|
+
| `status` | `RECEIPT_STATUSES` = `completed \| skipped \| deferred \| failed` | 同集合 |
|
|
315
|
+
| `outcome` | `RECEIPT_OUTCOMES` = `created \| updated \| mixed \| no_value` | 同集合 |
|
|
316
|
+
| `checked.{fromSeq, throughSeq}` | `checkedRange.{fromSeq, throughSeq}` | 字段名不同,值语义相同 |
|
|
317
|
+
| `createdIds / updatedIds / heldIds / reasonCode / retryable` | 同名 | 一致 |
|
|
318
|
+
| `checked.snapshotSha256` | 由 `snapshotHash` / `captureSessionSnapshot` 持有,回执里**不含** | 一致的分工:凭证身份不能由 worker 自报 |
|
|
319
|
+
|
|
320
|
+
两个形状差异记在这里,避免以后接真转录时猜:
|
|
321
|
+
|
|
322
|
+
1. **工具调用 id 的位置**:fixture 用顶层 `toolCallId`;宿主转录用 `event.data.callId`(`tool/call`)与 `event.data.message.callId`(`tool/result`),助手消息里的 `tool-call` 块用 `block.id`。本文的 `toolCallIdOf()` 与 `detectHalfStep()` 两种都认,所以半步判定在真转录上同样成立。
|
|
323
|
+
2. **半步判定**:两边都只看"有没有配对的 `tool/result`",都不读半份转录;C 包另外会补 `interruptedTurnClosers`,本文不做这件事——它只判 `deferred`,把"补齐"留给读盘的实现方。
|
|
324
|
+
|
|
325
|
+
接入方式:C 的 worker 跑完后用 `parseReceipt` 拿到 receipt,映射成本文 §10.1 的 payload,再 `runAb({ workers: { 'no-value': adapter } })` 注入即可;本文的凭证校验与游标规则会**再**检查一遍,不信任 worker 自报。
|
|
326
|
+
|
|
327
|
+
### 10.5 结构行:回执 → 凭证的回归
|
|
328
|
+
|
|
329
|
+
第五行 `guide-weighted:structural` 只换凭证来源,不换调度:worker 产出 C 包围栏格式的回执文本,`lib/consolidation-receipt.js` 的 `parseReceipt` 解析,`receiptToCredential` 映射成 §10.1 的 payload,之后照旧走 `validateCredential` / `canAdvance`。适配器自报的 id 仍不被信任。
|
|
330
|
+
|
|
331
|
+
- **默认不跑。** 默认报告仍是四行;结构行要用 `--row guide-weighted:structural` 显式指定,避免每次全量回放都多跑一遍回执编解码。
|
|
332
|
+
- **实测(2026-10-30)与 oracle 行逐 case 完全一致。** 14 个 case 的覆盖、检查次数、种子 token 全部相等(12 个 off case 合计 11/20 覆盖、6 次检查、4240 token;`deferred` 1、`failed` 0、凭证不合法 0)。也就是说,真 worker 的产出形状(回执 + 围栏)经 `parseReceipt` 后能被 §13.6 的凭证规则无损接受,不需要为它放宽规则。
|
|
333
|
+
|
|
334
|
+
三种失败映射:
|
|
335
|
+
|
|
336
|
+
| `parseReceipt` 结果 | 映射后 status | retryable | 落点 |
|
|
337
|
+
|---|---|---:|---|
|
|
338
|
+
| `receipt-missing`(没有回执) | `failed` | true | 游标不动 |
|
|
339
|
+
| `receipt-malformed`(围栏内 JSON 坏) | `failed` | true | 游标不动 |
|
|
340
|
+
| `receipt-invalid`(结构/枚举不符) | `failed` | true | 游标不动 |
|
|
341
|
+
|
|
342
|
+
三者都**不降级成 `no_value`**。理由是语义不同:`no_value` 表示"检查过、没有新事实",按 §10.2 会推进游标;空白或截断表示"没有结论",必须可重试,否则一次坏输出会被记成一次空检查,永久跳过那段范围。
|
|
343
|
+
|
|
344
|
+
一次完整 live 执行由两臂相加:baseline **13 次** + weighted **8 次** = **21 次 HTTP**。14 个 case 里,`clean-half-step` 两臂都被硬守卫挡住,weighted 另有 5 个 `scheduled-out`。任何真实模型复跑的成本都按这个数估。
|
|
345
|
+
|
|
346
|
+
已知语义分歧:C 包的 `decideCompletion` 认为带 `reasonCode` 的 `skipped` 也可以推进游标,本 harness 只认 `completed`。映射层因此把非 `completed` 的 id 桶一律清空——`skipped` 即使带了 `createdIds` 也不保留,`heldIds` 同样丢弃。这是有意的保守:宁可丢弃一次本可结算的 `skipped`,也不让未完成的检查被计成覆盖。接真 worker 时若要改这条,应当先改 §10.2 的游标规则,而不是在映射层偷偷放行。
|
|
347
|
+
|
|
348
|
+
## 11. 已知边界与未测项
|
|
349
|
+
|
|
350
|
+
- 主模型主动率(需要真实模型调用与用户明确授权);
|
|
351
|
+
- guide 正文对模型遵从性的影响;
|
|
352
|
+
- 后台 worker 的语义判断质量(oracle 适配器是上界;§9 的 precision 是词面匹配,不是语义判断);
|
|
353
|
+
- 真实 prompt 延迟与主任务阻塞耗时;
|
|
354
|
+
- 真实 token 消耗:确定性回放只有 estimate(按每次完整前缀 seed 累加);§9 的 prompt token 来自 provider usage,但只覆盖那一轮 fixture;
|
|
355
|
+
- 窗口化 seed 不是本 harness 的语义:生产 `private.js` 以完整连续前缀 `[0..throughSeq]` 作为 seed;逻辑资格范围仍单独由 `events` / `fromSeq..throughSeq` 表示;
|
|
356
|
+
- D 包的合并账本、跨重启恢复的真实案例未覆盖:这里的"重启后同一份快照"只体现为凭证身份校验,不是真机重放。
|
|
357
|
+
|
|
358
|
+
### 11.1 积压上界(`maxPendingEvents: 600`)
|
|
359
|
+
|
|
360
|
+
`assessWake` 新增 `pendingEvents` 维度:待整理区间达到 `maxPendingEvents`(默认 600 事件)时,在真人回合这道硬门槛之后、分数与大 transcript 守卫之前,无条件是检查一次,`reason` 为 `backlog`。
|
|
361
|
+
|
|
362
|
+
这条规则给延迟加上界,而不是放宽守卫。守卫要挡的是「为一句轻话重种整段转录」,不是「把从没整理过的大会话永远挡在门外」:没有上界的话,一个大会话只要每轮都是轻话,早期决定就永远等不到检查。被守卫跳过的区间游标不前移、内容仍在 pending,所以被跳过的是延迟而不是内容;`backlog` 只是把这段延迟封顶在 600 事件。
|
|
363
|
+
|
|
364
|
+
fixture 的 `snapshot.pendingEvents` 默认为 0,表示「已整理到区间起点、只有薄增量是新的」,因此 §5 的数字不受这条规则影响。
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 12. 本轮验证(2026-10-30)
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
node --check scripts/active-memory-ab.mjs ok
|
|
372
|
+
node --check scripts/active-memory-ab-live.mjs ok
|
|
373
|
+
node --check tests/active-memory-ab.test.mjs ok
|
|
374
|
+
node --import ./tests/loader-register.mjs --test tests/active-memory-ab.test.mjs
|
|
375
|
+
ℹ tests 38 / pass 38 / fail 0 / skipped 0
|
|
376
|
+
node scripts/active-memory-ab.mjs ok:true, structureChecks 为空, 14 cases
|
|
377
|
+
node scripts/active-memory-ab.mjs --row guide-weighted:structural
|
|
378
|
+
ok:true, 与 oracle 行逐 case 一致
|
|
379
|
+
node --import ./tests/loader-register.mjs scripts/active-memory-ab-live.mjs --dry-run
|
|
380
|
+
ok:true, dryRun:true, 14 cases, 计划 13 + 8 = 21 次
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
回归测试另外构造临时双检查 fixture(不改固定数据集):第二次 weighted 逻辑检查从 seq 10 开始,但所有 arm 的 seed 都是 `[0..19]` 完整连续前缀;同时断言 token 估算来自该前缀,证据与标签资格仍限于逻辑范围。两条 fork 路径的父会话等待都断言为 `null`,且 delta 中不允许伪造等待差异。
|
|
384
|
+
|
|
385
|
+
报告本身(`report.ok`)为 `true` 的含义只是:**没有结构违规**——区间连续、凭证合法、全局条目零泄漏。它**不**表示"主动记忆效果已通过验收"。
|