3 Commits

Author SHA1 Message Date
xiaoshuai 2d4df45acb memory/rules: 用「完全通过测试才提交」替换「改完立即推送」
- 新增 rules/only-commit-fully-tested.md(pinned):端到端跑通才算通过、
  禁止用 mock 冒充验证、已知缺陷不许推、如实写验证范围、不确定就说不确定
- 删除 rules/plugin-push-on-change.md:其「改完立刻 commit+push、不询问」
  与新规则的验证前提冲突
- 同步更新 memory/rules/index.md 的路由表

原规则里仍然有效的两条(改插件后 `opencode service restart`、推送走 SSH)
未写入新文件,需要的话另开一条记。
2026-10-12 18:10:48 +08:00
xiaoshuai 92c612b94c memory: fix extraction trigger, dedupe, and add runtime diagnostics
Four defects found by end-to-end testing, not by reading the code.

1. Extraction never ran, silently. Collecting every event type that
   ctx.event.subscribe actually delivers shows `session.idle` is NOT among
   them — it is defined in the protocol but never emitted on this stream.
   The subscription filtered on it alone, so auto-extraction was a permanent
   no-op with no error. Now debounced off events that do arrive
   (session.step.started / session.tool.called / session.inbox.delivered)
   after `settleMs` of quiet, which is what "idle" meant anyway. Verified:
   the trigger now fires and reports "轮数不足" for a 1-turn session.

2. Dedupe silently stopped rejecting duplicates. It compared against a fixed
   retrieval score, but that score carries idf and scope priors that drift as
   the corpus grows, so an obvious restatement of an existing memory scored
   0.48 against a 0.9 threshold and was written as a rival entry. Now uses
   term coverage of the candidate, which answers "is this already said?" and
   does not move with corpus size. Verified: a 100%-duplicate is rejected, a
   novel pitfall is kept.

3. Harness-injected user turns became the retrieval query. "<system-reminder>"
   and "The server restarted while you were working" are user-role messages,
   so the hook retrieved on them and injected memories chosen by noise. Both
   are filtered out now.

4. The `pinned` flag was absent from the remember tool's input schema, so the
   schema stripped it and pinning was unreachable from the tool surface.
   `kind` also ignored the rules/ category when a stale value was stored.

memory_inspect now reports runtime health for the two paths with no other
observable effect: last injection size and query, and last extraction outcome
with its reason. Without it, "working" and "silently dead" look identical —
which is exactly how defects 1 and 2 survived the first round of testing.
2026-10-09 10:28:34 +08:00
xiaoshuai a64ea2acb3 Add memory plugin: three-tier markdown store with plugin-driven retrieval
A durable memory layer for OpenCode. Structure:

  ROOT.md          total plan: goals, hard rules, directory registry,
                   and a scenario -> file routing table
  <category>/
    index.md       one line per file, COMPILED from frontmatter
    <slug>.md      the memory itself

Two roots, same shape: <config>/memory/ (global) and
<project canonical>/.memory/ (project). Project wins on conflict; global
hard rules stay resident everywhere.

Why a plugin rather than a skill that tells the model to read a folder:
model-driven retrieval is unreliable — the model forgets, reads half the
files, or reads the wrong ones. Markdown is the human-facing format; the
plugin is the engine that parses, scores, and injects.

Measured, not assumed: FTS5's two built-in tokenizers both fail on this
corpus. On SQLite 3.53.4 with "部署脚本用 pnpm 而不是 npm...": unicode61
emits 7 terms for the whole sentence and matches 0 hits for "部署";
trigram needs >= 3 characters and also returns 0 for that two-character
word. So tokenization happens in the plugin (CJK bigrams, latin words)
and no SQL index is used — a thousand memories are fine to scan.

index.md and ROOT's two tables are compiled, never hand-edited: a
hand-maintained index drifts, a generated one cannot. Hit counters live
in .state.json so cross-device sync does not conflict on every recall.

Tools are named recall/remember/forget/inspect rather than
write/read/status: generic names were observed shadowing the harness's
own built-in tools.
2026-10-09 10:17:45 +08:00