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.
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.