opencode-self-improvement 0.6.5 → 0.7.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 +46 -9
- package/package.json +5 -2
- package/src/inject.ts +30 -9
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,11 @@ prompt cache on every message. This one is built the other way around:
|
|
|
54
54
|
a compaction — and stays byte-identical in between. A stable system prefix
|
|
55
55
|
means the provider's prompt cache survives ordinary turns instead of being
|
|
56
56
|
invalidated on every message.
|
|
57
|
+
- **Two-concept evidence gate.** A free-text prompt must share at least two
|
|
58
|
+
distinct concepts with a memory for it to be injected; keyword-length
|
|
59
|
+
queries (one or two words — a deliberate search) need only one. Injection
|
|
60
|
+
stays quiet on single-word coincidences, which `memory_recall` still
|
|
61
|
+
reaches on the pull path — rare and right beats frequent and maybe.
|
|
57
62
|
- **Hard character budget.** The block is capped at 1200 characters by default
|
|
58
63
|
(see Options) and trimmed at whole-memory boundaries — never cut mid-sentence.
|
|
59
64
|
- **Evidence is never injected**, and the standing system digest is two lines.
|
|
@@ -67,10 +72,10 @@ store writes) using the plugin's real injection code:
|
|
|
67
72
|
|
|
68
73
|
| Policy | Recomputes | Injected chars (20 turns) | Avg/turn | Worst block |
|
|
69
74
|
| --- | ---: | ---: | ---: | ---: |
|
|
70
|
-
| A — recompute per prompt, uncapped (naive) | 20 |
|
|
71
|
-
| B — epoch freeze + 1200-char cap (shipped) | 4 |
|
|
75
|
+
| A — recompute per prompt, uncapped (naive) | 20 | 25742 | 1287.1 | 1838 |
|
|
76
|
+
| B — epoch freeze + 1200-char cap (shipped) | 4 | 19990 | 999.5 | 1139 |
|
|
72
77
|
|
|
73
|
-
Policy B injects
|
|
78
|
+
Policy B injects 22.3% fewer characters — and the bigger saving isn't even in
|
|
74
79
|
the character count: B's block is byte-identical between store changes, so the
|
|
75
80
|
prompt cache stays warm across ordinary turns. Reproduce (byte-identical
|
|
76
81
|
output):
|
|
@@ -123,23 +128,29 @@ right memory (store of 200; k=3 = the production injection limit):
|
|
|
123
128
|
|
|
124
129
|
| Query, vs. the stored memory | share | R@1 | R@3 |
|
|
125
130
|
| --- | ---: | ---: | ---: |
|
|
126
|
-
| lexical — reuses your project's words | 46% |
|
|
127
|
-
| near — reworded, one overlap | 17% |
|
|
131
|
+
| lexical — reuses your project's words | 46% | 98% | 98% |
|
|
132
|
+
| near — reworded, one overlap | 17% | 28% | 28% |
|
|
128
133
|
| strict — zero shared words | 37% | 0% | 0% |
|
|
129
134
|
|
|
130
135
|
What the benchmark bought us:
|
|
131
136
|
|
|
132
137
|
- **In-project prompts are overwhelmingly lexical** — you keep saying
|
|
133
138
|
`bun test`, "the rate limiter", "staging deploys". The shipped matcher
|
|
134
|
-
nails those: 100% R@3, flat from store size 50 to 500
|
|
139
|
+
nails those: 98–100% R@3, flat from store size 50 to 500, and the
|
|
140
|
+
two-concept evidence gate never abstains on one (0 of 48).
|
|
135
141
|
- **It caught a real bug.** Function words ("the", "what", "how") counted as
|
|
136
142
|
matches, so nearly half the time the top memory was the wrong one. The
|
|
137
143
|
matcher now ignores them on the query side: wrong-first rate fell 49% → 34%,
|
|
138
|
-
and
|
|
139
|
-
|
|
144
|
+
and the two-concept evidence gate + IDF ranking finished the job: **6%**.
|
|
145
|
+
- **One shared word is a coin flip, so injection stopped playing it.** At the
|
|
146
|
+
near class (a single shared concept) the old matcher force-pushed a guess;
|
|
147
|
+
free-text prompts now abstain and the pull path covers them. The trade,
|
|
148
|
+
exactly: near-class R@3 83% → 28% (the remainder is keyword-length queries,
|
|
149
|
+
which still inject on one concept) for the 34% → 6% wrong-memory number
|
|
150
|
+
above. A quiet turn beats a confident wrong one.
|
|
140
151
|
- **Strict paraphrase is the real lexical wall**, and we won't pretend
|
|
141
152
|
otherwise. Nothing keyword-side crosses it. So instead of confident noise the
|
|
142
|
-
matcher now goes *silent* when the vocabulary shares nothing (
|
|
153
|
+
matcher now goes *silent* when the vocabulary shares nothing (35 of 39
|
|
143
154
|
strict queries inject nothing at all), and the agent's `memory_recall` loop
|
|
144
155
|
remains as the pull path. Embeddings are the only known crossing — the bench
|
|
145
156
|
exposes a scorer seam if you want to measure them — but requiring an
|
|
@@ -211,6 +222,32 @@ After changes, `git pull` and restart opencode.
|
|
|
211
222
|
|
|
212
223
|
</details>
|
|
213
224
|
|
|
225
|
+
## Merge gate
|
|
226
|
+
|
|
227
|
+
Nothing merges without the full gate: CI runs it as required checks (`lint`,
|
|
228
|
+
`test`, `smoke`), and the same sequence is one command locally:
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
bun run gate # typecheck + lint + unit tests + terminal smoke & BDD
|
|
232
|
+
bun run gate:hooks # once per clone: run the gate automatically on `git push`
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Feature order is BDD-first.** Every new feature starts by adding (or fixing)
|
|
236
|
+
a BDD scenario in `e2e/` describing the behavior as a user would observe it —
|
|
237
|
+
before any implementation code and before unit tests. Then write the unit
|
|
238
|
+
tests for the pieces, then the code until everything is green. If no scenario
|
|
239
|
+
turns red first, the feature has no observable contract.
|
|
240
|
+
|
|
241
|
+
The `e2e/` suite is what keeps a load-error release from shipping again: it
|
|
242
|
+
boots the **real opencode CLI** against each candidate in a fully isolated
|
|
243
|
+
instance (fresh config, fresh db, fresh memory store, empty `HOME`) driven by
|
|
244
|
+
a scripted OpenAI-compatible fake model — no API keys anywhere. Scenarios
|
|
245
|
+
cover loading from the source tree and from the **packed npm artifact
|
|
246
|
+
installed runtime-deps-only** (the same semantics as opencode's plugin
|
|
247
|
+
cache), plus BDD flows for remember, recall, exact duplicates, near duplicates,
|
|
248
|
+
ungrounded claims, and forget. Needs `opencode` on `PATH` locally (or set
|
|
249
|
+
`OPENCODE_BIN`).
|
|
250
|
+
|
|
214
251
|
## Options
|
|
215
252
|
|
|
216
253
|
Zero configuration is the default. To tune the plugin, use the object form of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-self-improvement",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Durable memory for opencode: remember facts, decisions, and commands across sessions, plus a one-command per-repo setup (/repo-init).",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -57,7 +57,10 @@
|
|
|
57
57
|
"typescript": "^7.0.2"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"test": "bun test",
|
|
60
|
+
"test": "bun test ./test/",
|
|
61
|
+
"test:e2e": "bun test ./e2e/ --timeout 240000",
|
|
62
|
+
"gate": "bun scripts/merge-gate.ts",
|
|
63
|
+
"gate:hooks": "git config core.hooksPath .githooks",
|
|
61
64
|
"typecheck": "bunx tsc --noEmit -p tsconfig.json",
|
|
62
65
|
"bench": "bun bench/inject-cost.ts",
|
|
63
66
|
"bench:recall": "bun bench/recall-quality.ts",
|
package/src/inject.ts
CHANGED
|
@@ -82,23 +82,44 @@ export function words(text: string): string[] {
|
|
|
82
82
|
// Memory-side tokens are never filtered: a memory stating "the" must not
|
|
83
83
|
// gain or lose rank from it, only queries drive matching.
|
|
84
84
|
const QUERY_STOPWORDS = new Set(
|
|
85
|
-
"a an and are at be but by can did do does for from had has have how i in into is it its me my no not of on or our so than that the their them then there these they this to us was we were what when where which who why will with you your run runs getting want needs right back yet also just still ever".split(" "),
|
|
85
|
+
"a an and are at be but by can did do does for from had has have how i in into is it its me my no not of on or our so than that the their them then there these they this to us was we were what when where which who why will with you your run runs getting want needs right back yet also just still ever should would could might may must shall ought nothing something anything everything thing things".split(" "),
|
|
86
86
|
);
|
|
87
87
|
|
|
88
|
-
//
|
|
89
|
-
// memory
|
|
90
|
-
//
|
|
88
|
+
// A free-text prompt long enough to carry >= this many content tokens must
|
|
89
|
+
// share >= 2 distinct concepts with a memory to inject: with a long query
|
|
90
|
+
// vocabulary, one shared token is near-certain coincidence (a live store hit
|
|
91
|
+
// injected a git memory on the word "should", df=1). A keyword-length query
|
|
92
|
+
// is a deliberate search — one shared concept suffices. memory_recall and the
|
|
93
|
+
// contextual pull keep single-overlap matches reachable; injection just stays
|
|
94
|
+
// quiet unless the vocabulary genuinely agrees twice.
|
|
95
|
+
const LONG_QUERY_TOKENS = 3;
|
|
96
|
+
|
|
97
|
+
// Production relevance ranking: candidates must clear the concept-overlap
|
|
98
|
+
// gate, then rank by Robertson-IF of the shared tokens (rare concepts beat
|
|
99
|
+
// piles of common ones — the same idf form the bench's BM25 row uses).
|
|
100
|
+
// Exported for bench/recall-quality.ts — the benchmark measures the shipped
|
|
101
|
+
// matcher.
|
|
91
102
|
export function scoreMemories(term: string, mems: Memory[], limit: number): Memory[] {
|
|
92
103
|
const query = new Set(words(term).filter((t) => !QUERY_STOPWORDS.has(t)));
|
|
93
104
|
if (!query.size) return [];
|
|
105
|
+
const needShared = query.size >= LONG_QUERY_TOKENS ? 2 : 1;
|
|
106
|
+
const hays = mems.map((m) => new Set(words(`${m.scope} ${m.type} ${m.content}`)));
|
|
107
|
+
const df = new Map<string, number>();
|
|
108
|
+
for (const h of hays) for (const t of h) df.set(t, (df.get(t) ?? 0) + 1);
|
|
109
|
+
const n = mems.length || 1;
|
|
94
110
|
return mems
|
|
95
|
-
.map((m) => {
|
|
96
|
-
|
|
111
|
+
.map((m, i) => {
|
|
112
|
+
let shared = 0;
|
|
97
113
|
let score = 0;
|
|
98
|
-
for (const w of query)
|
|
99
|
-
|
|
114
|
+
for (const w of query)
|
|
115
|
+
if (hays[i]!.has(w)) {
|
|
116
|
+
shared++;
|
|
117
|
+
const d = df.get(w) ?? 0;
|
|
118
|
+
score += Math.log(1 + (n - d + 0.5) / (d + 0.5));
|
|
119
|
+
}
|
|
120
|
+
return { m, shared, score };
|
|
100
121
|
})
|
|
101
|
-
.filter((s) => s.
|
|
122
|
+
.filter((s) => s.shared >= needShared)
|
|
102
123
|
.sort((a, b) => b.score - a.score || openRank(b.m) - openRank(a.m))
|
|
103
124
|
.slice(0, limit)
|
|
104
125
|
.map((s) => s.m);
|
package/tsconfig.json
CHANGED