opencode-mnemon 0.0.0-stage → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CR903
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial parts of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,175 @@
1
- # Temporary Holding Version
1
+ # opencode-mnemon
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ OpenCode plugin for **project-scoped automatic memory** on top of the [mnemon](https://github.com/dsh) CLI.
4
+
5
+ Conversation turns are captured at runtime, distilled (rules first, LLM as fallback), written back through `mnemon remember`, and a gated slice of memory is re-injected at the start of every user prompt. No per-user setup: memories land in `<cwd>/.mnemon/` when the project has one, and fall back to `~/.mnemon` when it does not.
6
+
7
+ ## What it does
8
+
9
+ Two sides, sharing one mnemon store.
10
+
11
+ | Side | Trigger | Effect |
12
+ |---|---|---|
13
+ | **Write** | `session.text.ended` | Append the turn to a per-project runtime file, then after a 60s finalize window claim + extract + `mnemon remember` (async, BUSY backoff) |
14
+ | **Read** | `prompt` hook (every user message) | Inject a quality-gated recall section + cached one-line status + workspace-doc hints + recent runtime turns (≤3000 chars total) |
15
+
16
+ Three layers, mirroring dsh-mnemon:
17
+
18
+ ```
19
+ .mnemon/
20
+ runtime/ per-session scratch (this plugin owns opencode-memories.json)
21
+ data/ the mnemon store (sqlite graph)
22
+ documents/ read-only: indexed for recall, never auto-written
23
+ ```
24
+
25
+ ## Recall quality gate
26
+
27
+ `mnemon recall` has no `--min-score`, and `--limit N` pads to N with graph/temporal neighbours when intent auto-detects to WHY/WHEN/ENTITY — so the plugin gates results before injection (task 10-11):
28
+
29
+ - Messages under 6 non-whitespace characters skip the recall spawn entirely (`删` / `继续` measured zero evidence hits).
30
+ - Results are oversampled (`--limit 12 --verbose`), then filtered in order: invalid or `>1` scores drop, `< 0.25` drops, `< topScore − 0.15` drops, and items with `keyword+entity+similarity === 0` are treated as *padding* — dropped whenever any lexical hit exists. All padding → the whole section is omitted (one on-demand hint line remains).
31
+ - Kept results (≤ 4) render as compact lines (`— [0.38·preference] excerpt…`, section ≤ 900 chars); the ~30 KB verbose JSON only feeds `JSON.parse`, never the model context. The status section is compressed to one line (≤ 200 chars) and cached 60s per memory root.
32
+ - Empty or unparseable CLI output (observed `SQLITE_BUSY`) retries once after 150ms, then drops the section — injection never fails.
33
+ - The hint tells the model deep retrieval is on demand: `mnemon recall "<focused query>" --intent WHY|WHEN|ENTITY --limit 10 --verbose`.
34
+
35
+ This is noise reduction, not precision engineering: this store's scores cluster in 0.25–0.38, so the gate promises only that zero-signal padding never reaches the context.
36
+
37
+ ## Hard invariants
38
+
39
+ These are load-bearing. Breaking them corrupts shared state.
40
+
41
+ - **Never write to `runtime/memories.json`.** That file is dsh-mnemon's own runtime memory with a different schema (`{content, created_at, target, importance}`). OpenCode uses `runtime/opencode-memories.json` (+ `.opencode-memories.lock`) instead. The 50-entry trim would otherwise delete dsh's entries.
42
+ - **Never write to `documents/`.** Index it, don't own it.
43
+ - **Claim leases, not permanent marks.** Claims write `claimedAt`; a claim older than 300s can be taken over. Permanent marks have stalled 11 entries before.
44
+ - **Rule extract first, LLM only as a fallback.** `extractMemory(entry) ?? (await llmExtractMemory(entry))`. LLM calls never enter the writer lock, time out at 25s (`unref`), and fail silently back to the rule result.
45
+ - **LLM entry point is `ctx.generate.text`, not a session.** `ctx.session.prompt` is not on the V2 `ctx`, and `generate.text` does not re-enter the session prompt hook, so there is no recursion. The `[mnemon-extract]` marker is kept as a textual backstop.
46
+
47
+ ## Deployment model
48
+
49
+ One real file, symlinked into the OpenCode plugin slot — so editing the repo edits what OpenCode loads and nothing can drift.
50
+
51
+ ```sh
52
+ npm run deploy # ~/.config/opencode/plugins/mnemon.js -> ../opencode-mnemon/mnemon.js
53
+ npm run deploy -- --copy # force a real copy instead
54
+ ```
55
+
56
+ The deploy script never leaves the slot momentarily empty: the new link is created at a temp path and swapped in with `rename()`.
57
+
58
+ ## Testing
59
+
60
+ ```sh
61
+ npm test # rebuild the test copy, then run all four suites
62
+ npm run test:deploy # same, but against $PWD/mnemon.js instead of the live deploy
63
+ ```
64
+
65
+ Suites (182 checks total):
66
+
67
+ | File | Coverage |
68
+ |---|---|
69
+ | `tests/mnemon-recall-gate-test.mjs` | Recall gate: query floor, score/band/signal filtering, compact-line budgets, status compression + TTL cache |
70
+ | `tests/mnemon-extract-test.mjs` | Rule extraction: headlines, entities, categories, fence ratio, dedupe |
71
+ | `tests/mnemon-remember-e2e.mjs` | Real `mnemon remember --data-dir` round trip; global-root isolation |
72
+ | `tests/mnemon-llm-extract-test.mjs` | LLM switch matrix, model catalog resolution, prompt/parse, timeout, silent fallback |
73
+
74
+ Tests import `/tmp/mnemon-test-plugin.mjs`, a copy of the plugin with internals appended as exports. **Always run `tests/build-mnemon-test.sh` after editing** — otherwise you are testing a stale copy. The production file stays in its clean `{ id, setup, server }` shape.
75
+
76
+ `build-mnemon-test.sh` accepts the source path as `$1` or `$MNEMON_PLUGIN_PATH`; with neither, it uses the live deployment and falls back to the repo copy when no deployment exists (fresh clone, CI). Set `MNEMON_REQUIRE_DEPLOYMENT=1` to disable that fallback.
77
+
78
+ ### Environment overrides
79
+
80
+ | Variable | Effect |
81
+ |----------|--------|
82
+ | `MNEMON_BIN` | Path to the `mnemon` CLI the e2e suite drives. Defaults to bare `mnemon` on `PATH`. |
83
+ | `MNEMON_TASK_DIR_OPTIONAL=1` | Allow `sync:docs:check` to skip when no Trellis task directory exists. Without it the check fails, since a comparison against nothing is not a pass. |
84
+ | `MNEMON_TEST_DST` | Where the exported test copy is written. Defaults to `/tmp/mnemon-test-plugin.mjs`. |
85
+
86
+ CI (`.github/workflows/test.yml`) installs `@mnemon-dev/mnemon` from npm — it is publicly published with per-platform binaries and needs no credentials — then runs the suite under a scratch `HOME` so the fallback assertions cannot touch a real memory root.
87
+
88
+ ## Switches
89
+
90
+ | Switch | Scope | Timing |
91
+ |---|---|---|
92
+ | `<root>/opencode-llm-extract` | per-project | `touch` on, `rm` off, next sweep — no restart |
93
+ | `MNEMON_LLM_EXTRACT=1` | process | restart required |
94
+ | `MNEMON_LLM_EXTRACT=0` | process | master kill, overrides the flag file |
95
+ | `MNEMON_LLM_MODEL` | process | overrides the flag file's model content |
96
+ | `MNEMON_AUTOMEM=0` | process | write side off; **read side stays on** |
97
+ | `MNEMON_RECALL=0` | process | recall section off; status / docs / runtime / hint stay on |
98
+ | `TRELLIS_HOOKS=0` / `TRELLIS_DISABLE_HOOKS=1` | process | master kill, **both sides stop** |
99
+
100
+ The flag file's non-empty content is treated as a model ID.
101
+
102
+ **Env switches are process-level.** Every plugin instance lives in the long-running OpenCode *server* process, so `TRELLIS_HOOKS=0 opencode run ...` does nothing — `opencode run` is a client and the env never reaches the server. Export it in the environment that starts the server. This also means env switches cannot be scoped per-project; use the flag file for that.
103
+
104
+ ## LLM model resolution
105
+
106
+ Models are resolved against the live catalog, never hardcoded — a hardcoded provider that is not in `ctx.model.list()` fails every call.
107
+
108
+ - `pickModelRef(models, wanted)` searches `[configured, ...LLM_MODEL_CHAIN]` in order. Empty catalog → trust the config; nothing matches → `null` and a silent give-up.
109
+ - `LLM_MODEL_CHAIN = ["opencode-go/qwen3.8-flash", "opencode-go/deepseek-v4-flash", "opencode-gemini-3.5-flash-lite", "opencode-go/longcat-2.5-preview-free"]`
110
+ - `ctx.model.list()` returns `{ location, data }` — take `.data`.
111
+ - The model ref must be `{ id, modelID, providerID }` and **`id` must be the bare modelID**. Passing the full `provider/model` string gets re-joined into `provider/provider/model`. `{ id }` alone fails with `Missing key at ["model"]["providerID"]`; a bare string fails with `Expected Model.Ref | null`. Omitting `model` hits the free tier, which rejects out-of-OpenCode calls.
112
+
113
+ All of the above was found by probing, not reading docs.
114
+
115
+ ## mnemon invocation
116
+
117
+ `mnemon` on PATH is an npm JS launcher; `SIGKILL`ing it leaves the native child orphaned. `mnemonCommand()` resolves the real native binary once and caches it: `realpathSync` detects `bin/mnemon.js` → reads `targets.json` for the platform/arch match → `createRequire` resolves the optional dep. On win32 it goes through the `.cmd` shim branch. Resolves to e.g. `.../@mnemon-dev/mnemon-darwin-x64/bin/mnemon`.
118
+
119
+ ## Timing constants
120
+
121
+ | Constant | Value |
122
+ |---|---|
123
+ | `MIN_AGE_MS` / `FINALIZE_MS` | 60 000 |
124
+ | `BATCH` | 3 |
125
+ | `MIN_TOTAL_CHARS` | 24 |
126
+ | `SUBSTANTIVE_CHARS` | 60 |
127
+ | `MAX_FENCE_RATIO` | 0.7 |
128
+ | `ASSISTANT_CHARS` / `USER_CHARS` / `HEADLINE_CHARS` | 700 / 150 / 110 |
129
+ | `MAX_ENTITIES` / `MIN_IMP` | 8 / 3 |
130
+ | `BUSY_RETRY_MS` | 350 |
131
+ | `TAIL_DELAY_MS` | 75 000 |
132
+ | `CLAIM_LEASE_MS` | 300 000 |
133
+ | `MIN_RUNTIME_TEXT_CHARS` | 20 |
134
+ | `RUNTIME_RECENT_COUNT` | 5 |
135
+ | `LLM_TIMEOUT_MS` / `LLM_MAX_ENTITIES` | 25 000 / 6 |
136
+ | `RECALL_LIMIT` / `RECALL_MAX` | 12 / 4 |
137
+ | `RECALL_MIN_SCORE` / `RECALL_BAND` | 0.25 / 0.15 |
138
+ | `RECALL_MIN_QCHARS` | 6 |
139
+ | `STATUS_TTL_MS` | 60 000 |
140
+ | `RECALL_SECTION_MAX_CHARS` / `STATUS_SECTION_MAX_CHARS` | 900 / 200 |
141
+
142
+ `MIN_RUNTIME_TEXT_CHARS` drops short turns **silently** — a probe that replies `好` will look like a broken pipeline. It is not.
143
+
144
+ ## Debugging
145
+
146
+ - Plugin log: `/tmp/trellis-plugin-debug.log`, grep `mnemon-auto` (append / remember / `llm extract ok|drop|skipped` / sweep / lease / `recall gate: kept= dropped= padding= reason= ms= chars= status=cached|spawn`). Rotates at 8 MB to `.log.1` / `.log.2`, so an investigation can still read the previous generation after a rotation lands mid-diagnosis.
147
+ - Load log: `~/.local/share/opencode/log/opencode.log`, `loading plugin` / `failed to load plugin` — the raw counts are inflated; filter with `grep -av "spawning process"`
148
+
149
+ ## Project conventions
150
+
151
+ - Config files (JSON, `.env`, `package.json`) use **tabs**, not spaces.
152
+ - This repo is the single source of truth. There is no per-project copy: `BalanceDeck/.opencode/plugins/mnemon.js` was deleted as a stale fork.
153
+
154
+ ## Documentation
155
+
156
+ `docs/prd.md`, `docs/design.md`, `docs/implement.md` hold the requirements, technical design, and the execution record with verification evidence. The Trellis task directory at `~/project/others/.trellis/tasks/10-11-mnemon-recall-gate/` is the working copy; `docs/` is a one-way snapshot of it, kept in git because that task directory is not version controlled anywhere.
157
+
158
+ Edit the task documents, then sync:
159
+
160
+ ```
161
+ npm run sync:docs # copy prd/design/implement into docs/
162
+ npm run sync:docs:check # report drift only; non-zero exit on drift
163
+ ```
164
+
165
+ `npm test` runs the check, so a stale snapshot fails the suite. `docs/` is derived: never edit it directly, your changes will be overwritten on the next sync.
166
+
167
+ The script finds the task directory whether it is still active (`tasks/<slug>/`) or already archived (`tasks/archive/<YYYY-MM>/<slug>/`) — archiving moves it, and an archived task is still the source of record. Override with `MNEMON_TASK_DIR=...` or a positional path.
168
+
169
+ **If no task directory is found, the check fails rather than passing.** A check that cannot find its source has verified nothing, and exiting clean would read as protection while checking nothing. On a machine that genuinely has no Trellis task directory, opt out explicitly:
170
+
171
+ ```
172
+ MNEMON_TASK_DIR_OPTIONAL=1 npm test
173
+ ```
174
+
175
+ Not covered by the sync: `tests/`. Those are live code here, not a snapshot, so they are never written from the task directory. `build-mnemon-test.sh` intentionally differs from the archived copy — the repo version accepts `MNEMON_PLUGIN_PATH` and a path argument and validates the source exists, while the archived one hardcodes the live deployment path. The archived copy stays as it is: it is the frozen record of what that task produced, and rewriting it would misrepresent that.
@@ -0,0 +1,175 @@
1
+ # opencode-mnemon
2
+
3
+ 基于 [mnemon](https://github.com/dsh) CLI 的 OpenCode 插件,提供**按项目隔离的自动记忆**。
4
+
5
+ 对话回合在运行时被捕获、蒸馏(先规则、LLM 兜底),通过 `mnemon remember` 写回,并在每次用户消息开头重新注入一段经过质量闸门筛选的记忆。无需逐个用户配置:当项目存在 `<cwd>/.mnemon/` 时记忆落在项目本地,否则回退到 `~/.mnemon`。
6
+
7
+ ## 它做什么
8
+
9
+ 读写两侧共享同一个 mnemon 存储。
10
+
11
+ | 侧 | 触发 | 效果 |
12
+ |---|---|---|
13
+ | **写** | `session.text.ended` | 把回合追加到项目级 runtime 文件,60s 收尾窗口后 claim + 抽取 + `mnemon remember`(异步,SQLite BUSY 退避) |
14
+ | **读** | `prompt` hook(每次用户消息) | 注入质量闸门筛选的召回段 + 缓存的一行状态 + 工作区文档提示 + 最近运行时回合(总计 ≤3000 字符) |
15
+
16
+ 三层结构,与 dsh-mnemon 对齐:
17
+
18
+ ```
19
+ .mnemon/
20
+ runtime/ 每会话临时区(本插件持有 opencode-memories.json)
21
+ data/ mnemon 存储(sqlite 图)
22
+ documents/ 只读:参与召回索引,绝不自动写入
23
+ ```
24
+
25
+ ## 召回质量闸门
26
+
27
+ `mnemon recall` 没有 `--min-score`,且 `--limit N` 在意图命中 WHY/WHEN/ENTITY 时用图/时间邻居补到 N——所以插件在注入前先做闸门过滤(task 10-11):
28
+
29
+ - 少于 6 个非空白字符的消息完全跳过召回(实测「删」/「继续」零证据命中)。
30
+ - 结果过采样(`--limit 12 --verbose`),然后依次过滤:非法或 `>1` 的分数丢弃,`< 0.25` 丢弃,`< topScore − 0.15` 丢弃,`keyword+entity+similarity === 0` 视为 *padding*——只要有词法命中就丢弃 padding。全部都是 padding → 整段省略(只留一行按需检索提示)。
31
+ - 保留结果(≤ 4 条)渲染为紧凑行(`— [0.38·preference] excerpt…`,段 ≤ 900 字符);约 30KB 的 verbose JSON 只喂 `JSON.parse`,绝不进模型上下文。状态段压缩为一行(≤ 200 字符),按记忆根缓存 60s。
32
+ - CLI 输出为空或不可解析(实测 `SQLITE_BUSY`)时 150ms 后重试一次,仍失败则丢弃该段——注入永不失败。
33
+ - 提示告诉模型深度检索是按需的:`mnemon recall "<focused query>" --intent WHY|WHEN|ENTITY --limit 10 --verbose`。
34
+
35
+ 这是降噪,不是精确工程:本库分数聚集在 0.25–0.38,闸门只承诺零信号 padding 绝不进上下文。
36
+
37
+ ## 硬性不变量
38
+
39
+ 这些是承重墙。破坏它们会污染共享状态。
40
+
41
+ - **绝不写 `runtime/memories.json`。** 该文件是 dsh-mnemon 自己的运行时记忆,schema 不同(`{content, created_at, target, importance}`)。OpenCode 使用 `runtime/opencode-memories.json`(+ `.opencode-memories.lock`)。否则 50 条修剪会删掉 dsh 的条目。
42
+ - **绝不写 `documents/`。** 只建索引,不拥有它。
43
+ - **用租约,不用永久标记。** claim 写 `claimedAt`;超过 300s 的 claim 可被接管。永久标记曾卡住 11 条。
44
+ - **先规则抽取,LLM 只兜底。** `extractMemory(entry) ?? (await llmExtractMemory(entry))`。LLM 调用永不进入写锁,25s 超时(`unref`),失败静默回退规则结果。
45
+ - **LLM 入口是 `ctx.generate.text`,不是 session。** V2 `ctx` 上没有 `ctx.session.prompt`,`generate.text` 不会重新进入会话 prompt hook,所以没有递归。`[mnemon-extract]` 标记保留为文本级兜底。
46
+
47
+ ## 部署模型
48
+
49
+ 一个真实文件,符号链接进 OpenCode 插件槽——编辑仓库即编辑生产,永无漂移。
50
+
51
+ ```sh
52
+ npm run deploy # ~/.config/opencode/plugins/mnemon.js -> ../opencode-mnemon/mnemon.js
53
+ npm run deploy -- --copy # 强制真实复制
54
+ ```
55
+
56
+ 部署脚本绝不会让槽位瞬间为空:新链接先建在临时路径,再用 `rename()` 原子换入。
57
+
58
+ ## 测试
59
+
60
+ ```sh
61
+ npm test # 重建测试副本,然后跑全部四个套件
62
+ npm run test:deploy # 同上,但针对 $PWD/mnemon.js 而不是 live 部署
63
+ ```
64
+
65
+ 套件(共 182 项检查):
66
+
67
+ | 文件 | 覆盖 |
68
+ |---|---|
69
+ | `tests/mnemon-recall-gate-test.mjs` | 召回闸门:查询下限、分数/带宽/信号过滤、紧凑行预算、状态压缩 + TTL 缓存 |
70
+ | `tests/mnemon-extract-test.mjs` | 规则抽取:标题、实体、类别、围栏比例、去重 |
71
+ | `tests/mnemon-remember-e2e.mjs` | 真实 `mnemon remember --data-dir` 往返;全局根隔离 |
72
+ | `tests/mnemon-llm-extract-test.mjs` | LLM 开关矩阵、模型目录解析、prompt/解析、超时、静默回退 |
73
+
74
+ 测试从 `/tmp/mnemon-test-plugin.mjs` 导入——这是把内部实现附加为导出的插件副本。**编辑后务必运行 `tests/build-mnemon-test.sh`**,否则测的是过期副本。生产文件保持干净的 `{ id, setup, server }` 形状。
75
+
76
+ `build-mnemon-test.sh` 接受 `$1` 或 `$MNEMON_PLUGIN_PATH` 作为源路径;都不给时用 live 部署,无部署时回退仓库副本(全新 clone / CI)。设 `MNEMON_REQUIRE_DEPLOYMENT=1` 禁用回退。
77
+
78
+ ### 环境变量覆盖
79
+
80
+ | 变量 | 作用 |
81
+ |---|---|
82
+ | `MNEMON_BIN` | e2e 套件驱动的 `mnemon` CLI 路径。默认取 `PATH` 上的裸 `mnemon`。 |
83
+ | `MNEMON_TASK_DIR_OPTIONAL=1` | 允许 `sync:docs:check` 在不存在 Trellis 任务目录时跳过。不设则失败——对不存在的东西做比对不是通过。 |
84
+ | `MNEMON_TEST_DST` | 导出的测试副本写入位置。默认 `/tmp/mnemon-test-plugin.mjs`。 |
85
+
86
+ CI(`.github/workflows/test.yml`)从 npm 安装 `@mnemon-dev/mnemon`——它公开发布、带各平台二进制、无需凭据——然后在临时 `HOME` 下跑套件,让回退断言碰不到真实记忆根。
87
+
88
+ ## 开关
89
+
90
+ | 开关 | 范围 | 时机 |
91
+ |---|---|---|
92
+ | `<root>/opencode-llm-extract` | 按项目 | `touch` 开,`rm` 关,下轮 sweep 生效——无需重启 |
93
+ | `MNEMON_LLM_EXTRACT=1` | 进程 | 需重启 |
94
+ | `MNEMON_LLM_EXTRACT=0` | 进程 | 总闸,压过标记文件 |
95
+ | `MNEMON_LLM_MODEL` | 进程 | 压过标记文件的模型内容 |
96
+ | `MNEMON_AUTOMEM=0` | 进程 | 写侧关闭;**读侧保持开启** |
97
+ | `MNEMON_RECALL=0` | 进程 | 召回段关闭;状态 / 文档 / 运行时 / 提示保留 |
98
+ | `TRELLIS_HOOKS=0` / `TRELLIS_DISABLE_HOOKS=1` | 进程 | 总闸,**读写两侧都停止** |
99
+
100
+ 标记文件的非空内容被视为模型 ID。
101
+
102
+ **环境开关是进程级的。** 每个插件实例都活在后端常驻的 OpenCode *server* 进程里,所以 `TRELLIS_HOOKS=0 opencode run ...` 无效——`opencode run` 是客户端,环境变量到不了 server。请在启动 server 的环境里导出。这也意味着环境开关无法按项目隔离;要按项目用标记文件。
103
+
104
+ ## LLM 模型解析
105
+
106
+ 模型按 live 目录解析,绝不硬编码——硬编码一个不在 `ctx.model.list()` 里的 provider,每次调用都会失败。
107
+
108
+ - `pickModelRef(models, wanted)` 按 `[configured, ...LLM_MODEL_CHAIN]` 顺序搜索。目录为空 → 信任配置;无匹配 → `null` 并静默放弃。
109
+ - `LLM_MODEL_CHAIN = ["opencode-go/qwen3.8-flash", "opencode-go/deepseek-v4-flash", "opencode-gemini-3.5-flash-lite", "opencode-go/longcat-2.5-preview-free"]`
110
+ - `ctx.model.list()` 返回 `{ location, data }` ——取 `.data`。
111
+ - 模型引用必须是 `{ id, modelID, providerID }` 且 **`id` 必须是裸 modelID**。传完整 `provider/model` 字符串会被重新拼成 `provider/provider/model`。只有 `{ id }` 会报 `Missing key at ["model"]["providerID"]`;裸字符串报 `Expected Model.Ref | null`;省略 `model` 命中免费档,它会拒绝 OpenCode 外部的调用。
112
+
113
+ 以上全部是探测出来的,不是读文档读出来的。
114
+
115
+ ## mnemon 调用
116
+
117
+ `PATH` 上的 `mnemon` 是 npm JS 启动器;`SIGKILL` 它会留下孤儿原生子进程。`mnemonCommand()` 解析真实原生二进制一次并缓存:`realpathSync` 检测 `bin/mnemon.js` → 读 `targets.json` 找平台/架构匹配 → `createRequire` 解析 optional 依赖。win32 走 `.cmd` shim 分支。解析到例如 `.../@mnemon-dev/mnemon-darwin-x64/bin/mnemon`。
118
+
119
+ ## 时序常量
120
+
121
+ | 常量 | 值 |
122
+ |---|---|
123
+ | `MIN_AGE_MS` / `FINALIZE_MS` | 60 000 |
124
+ | `BATCH` | 3 |
125
+ | `MIN_TOTAL_CHARS` | 24 |
126
+ | `SUBSTANTIVE_CHARS` | 60 |
127
+ | `MAX_FENCE_RATIO` | 0.7 |
128
+ | `ASSISTANT_CHARS` / `USER_CHARS` / `HEADLINE_CHARS` | 700 / 150 / 110 |
129
+ | `MAX_ENTITIES` / `MIN_IMP` | 8 / 3 |
130
+ | `BUSY_RETRY_MS` | 350 |
131
+ | `TAIL_DELAY_MS` | 75 000 |
132
+ | `CLAIM_LEASE_MS` | 300 000 |
133
+ | `MIN_RUNTIME_TEXT_CHARS` | 20 |
134
+ | `RUNTIME_RECENT_COUNT` | 5 |
135
+ | `LLM_TIMEOUT_MS` / `LLM_MAX_ENTITIES` | 25 000 / 6 |
136
+ | `RECALL_LIMIT` / `RECALL_MAX` | 12 / 4 |
137
+ | `RECALL_MIN_SCORE` / `RECALL_BAND` | 0.25 / 0.15 |
138
+ | `RECALL_MIN_QCHARS` | 6 |
139
+ | `STATUS_TTL_MS` | 60 000 |
140
+ | `RECALL_SECTION_MAX_CHARS` / `STATUS_SECTION_MAX_CHARS` | 900 / 200 |
141
+
142
+ `MIN_RUNTIME_TEXT_CHARS` 会**静默**丢弃过短回合——探针回个「好」看起来像管线坏了。其实不是。
143
+
144
+ ## 调试
145
+
146
+ - 插件日志:`/tmp/trellis-plugin-debug.log`,grep `mnemon-auto`(append / remember / `llm extract ok|drop|skipped` / sweep / lease / `recall gate: kept= dropped= padding= reason= ms= chars= status=cached|spawn`)。8MB 轮转到 `.log.1` / `.log.2`,诊断中途轮转也能读上一代。
147
+ - 加载日志:`~/.local/share/opencode/log/opencode.log`,`loading plugin` / `failed to load plugin`——原始计数有虚高,用 `grep -av "spawning process"` 过滤。
148
+
149
+ ## 项目约定
150
+
151
+ - 配置文件(JSON、`.env`、`package.json`)用 **tabs**,不用空格。
152
+ - 本仓库是唯一事实来源。没有项目级副本:`BalanceDeck/.opencode/plugins/mnemon.js` 已作为过时 fork 删除。
153
+
154
+ ## 文档
155
+
156
+ `docs/prd.md`、`docs/design.md`、`docs/implement.md` 保存需求、技术设计与带验证证据的执行记录。Trellis 任务目录 `~/project/others/.trellis/tasks/10-11-mnemon-recall-gate/` 是工作副本;`docs/` 是它的一向快照,因为该任务目录不受任何版本控制,所以 `docs/` 留在 git 里。
157
+
158
+ 编辑任务文档后同步:
159
+
160
+ ```
161
+ npm run sync:docs # 把 prd/design/implement 复制到 docs/
162
+ npm run sync:docs:check # 只报漂移;有漂移非零退出
163
+ ```
164
+
165
+ `npm test` 会跑这个检查,所以快照过期套件就会失败。`docs/` 是派生品:绝不要直接编辑它,下次同步会覆盖。
166
+
167
+ 脚本无论任务目录还在活跃(`tasks/<slug>/`)还是已归档(`tasks/archive/<YYYY-MM>/<slug>/`)都能找到——归档会移动它,而已归档任务仍是事实来源。用 `MNEMON_TASK_DIR=...` 或位置参数覆盖。
168
+
169
+ **找不到任务目录时检查失败而不是通过。** 找不到源就等于没验证任何东西,干净退出看起来像在保护却什么都没查。在确实没有 Trellis 任务目录的机器上,显式退出:
170
+
171
+ ```
172
+ MNEMON_TASK_DIR_OPTIONAL=1 npm test
173
+ ```
174
+
175
+ `sync:` 不覆盖 `tests/`。它们是这里的活代码,不是快照,绝不会从任务目录写入。`build-mnemon-test.sh` 有意与归档副本不同——仓库版本接受 `MNEMON_PLUGIN_PATH` 和路径参数并校验源存在,归档版硬编码 live 部署路径。归档副本保持原样:它是该任务产出的冻结记录,改写它就是在歪曲历史。