open-memex 0.6.1 → 0.6.2
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 +3 -1
- package/CHANGELOG.md +142 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/doctor.js +130 -1
- package/docs/V2-DESIGN.md +17 -4
- package/package.json +1 -1
- package/src/doctor.ts +129 -1
package/AGENTS.md
CHANGED
|
@@ -64,7 +64,9 @@ resolves the server command
|
|
|
64
64
|
at init time — npx fallback when no durable bin is on PATH, D17),
|
|
65
65
|
`open-memex config` prints the effective config, `open-memex capture --dry-run "text"`
|
|
66
66
|
previews keyword capture without writing, `open-memex doctor` runs health checks
|
|
67
|
-
(node version, config, scope resolution, storage writability,
|
|
67
|
+
(node version, config, scope resolution, storage writability, SQLite locking,
|
|
68
|
+
opencode plugin entry (target exists + no host-SDK runtime imports, D59),
|
|
69
|
+
VS Code MCP enablement, MCP handshake).
|
|
68
70
|
`open-memex init` with no --client auto-detects and wires every installed editor
|
|
69
71
|
(`--yes` skips, scripts never prompt); `open-memex config set <key> <value>` edits settings after install.
|
|
70
72
|
`npm install -g` prints a pointer to `open-memex init` via a postinstall script
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to open-memex are recorded here. Entries are grouped by
|
|
4
|
+
the design decision (D<n>) that drove them, matching the
|
|
5
|
+
[GitHub Releases](https://github.com/stoneskin/open-memex/releases); the
|
|
6
|
+
rationale behind each decision lives in the decision log in
|
|
7
|
+
[docs/V2-DESIGN.md](docs/V2-DESIGN.md). Pre-release builds published under
|
|
8
|
+
the `@alpha` tag are development snapshots and are not listed individually.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.6.1] - 2026-10-03
|
|
13
|
+
|
|
14
|
+
## Question-shaped search, host parity said out loud (D56)
|
|
15
|
+
|
|
16
|
+
- Search no longer feeds whole questions straight into full-text: query construction filters English/CJK function words, dedupes terms, and caps them — asking "how do we configure…" or "中文记忆怎么检索" now ranks the right memory first instead of matching everything
|
|
17
|
+
- The six MCP-only tools now say so in their own descriptions and name the CLI equivalent, so opencode plugin users see the asymmetry is by design instead of assuming breakage
|
|
18
|
+
- `init`'s closing lines state the privacy model in plain words: memories live only on this machine, personal ones never leave it, nothing is uploaded
|
|
19
|
+
|
|
20
|
+
## Search explainability and memory audit (D57)
|
|
21
|
+
|
|
22
|
+
- `open-memex search "deploy" --explain` shows the constructed FTS expression, per-hit scores, and how many matches lifecycle hid (superseded vs retracted/archived) — a miss is now diagnosable
|
|
23
|
+
- New `open-memex audit` (read-only; `doctor` checks the environment, this checks the memories): near-duplicate active pairs, memories untouched for 90+ days, broken supersede chains, personal files found inside the repo memory dir, index/file drift
|
|
24
|
+
- Concurrency posture measured and hardened: WAL everywhere, `busy_timeout` unified at 5s across backends (the opencode plugin's bun:sqlite defaulted to 0 — any overlapping write failed instantly), memory files written via tmp+rename, lock timeouts reported in one plain line, `doctor` reports the live locking settings
|
|
25
|
+
|
|
26
|
+
## Critical fix: opencode plugin silently dead on global installs (D58)
|
|
27
|
+
|
|
28
|
+
- After a global `npm i -g open-memex`, the opencode plugin was configured but silently inactive — no memory tools, no error, nothing in the log. Root cause: the plugin runtime-imported the host SDK (`@opencode-ai/plugin` is a devDependency; nothing resolves it from a global install, and opencode skips the plugin without a trace). The import is gone — the SDK helper is a runtime identity, now a local stand-in. **If your opencode shows no memory tools, update to 0.6.1.**
|
|
29
|
+
|
|
30
|
+
## [0.6.0] - 2026-10-02
|
|
31
|
+
|
|
32
|
+
## First-run onboarding (D50, D51, F28)
|
|
33
|
+
|
|
34
|
+
- `open-memex init` is now a guided first run: the postinstall note points at init, every CLI entry prints a one-line stderr nudge until init has run or been declined (stderr keeps the MCP stdio protocol intact), and init ends with a next-step hint (`open-memex add` + ask the agent what it remembers)
|
|
35
|
+
- Bare `open-memex` on a machine where init never completed offers to run it on a TTY; init/uninstall maintain a `.init.json` first-run marker so the offer is asked once
|
|
36
|
+
|
|
37
|
+
## Agent prompt clarity (D52)
|
|
38
|
+
|
|
39
|
+
- MCP tool descriptions rewritten for clarity
|
|
40
|
+
|
|
41
|
+
## Push-not-poll outbox (D53)
|
|
42
|
+
|
|
43
|
+
- New project memories land in a local outbox (invisible to git); `open-memex sync-status` shows drafts waiting and `open-memex submit` publishes them — the agent checks on demand instead of polling
|
|
44
|
+
|
|
45
|
+
## Agent Skills (D54, D55)
|
|
46
|
+
|
|
47
|
+
- New bundled `open-memex` skill (`skills/open-memex/SKILL.md`): teaches skill-aware agents the CLI (save/search/scope rules/outbox flow), preferring MCP tools when available; `init` installs it per editor, `uninstall` removes it
|
|
48
|
+
- opencode with the native plugin wired no longer gets the skill — the plugin already provides memory tools, and the duplicated guidance made the agent chatty
|
|
49
|
+
|
|
50
|
+
## Rename leftovers swept (F30)
|
|
51
|
+
|
|
52
|
+
- The my-o-memory → open-memex rename left editor configs loading the OLD plugin/server, silently splitting memories across two data dirs; `init`/`uninstall` now drop stale `my-o-memory` entries wherever they touch a config, and `doctor` reports leftovers via a read-only `legacy my-o-memory` check
|
|
53
|
+
|
|
54
|
+
## Fixes (F29)
|
|
55
|
+
|
|
56
|
+
- Copilot review fixes, package-lock.json repair, Visual Studio onboarding strings
|
|
57
|
+
|
|
58
|
+
## [0.5.1] - 2026-10-01
|
|
59
|
+
|
|
60
|
+
## Docs review (F1–F27)
|
|
61
|
+
|
|
62
|
+
- Full documentation pass: stale versions corrected (README now shows 0.5.0), roadmap extended with 0.5.0 (D45–D49), branch rules updated after the V2 branch retirement, MCP server tool count fixed to 11, USER-GUIDE cleaned of V2-dev leftovers
|
|
63
|
+
|
|
64
|
+
## `mcp --help` accuracy (F27)
|
|
65
|
+
|
|
66
|
+
- `--help` now states the MCP server exposes 11 tools (a superset of the opencode plugin's five `memory_*` tools); install hints point at the stable line instead of `@alpha`
|
|
67
|
+
|
|
68
|
+
## doctor: VS Code MCP-disabled check
|
|
69
|
+
|
|
70
|
+
- `open-memex doctor` now fails with a clear message when VS Code has MCP disabled — via `chat.mcp.enabled: false` in user/project `settings.json`, or a Windows registry policy (`HKLM/HKCU\SOFTWARE\Policies\Microsoft\VSCode`)
|
|
71
|
+
|
|
72
|
+
## [0.5.0] - 2026-09-30
|
|
73
|
+
|
|
74
|
+
## One config for every project (D45)
|
|
75
|
+
|
|
76
|
+
- `open-memex init --global`: writes editor wiring once at user level — one setup, every project covered
|
|
77
|
+
- `npm install -g open-memex` puts the command in your PATH; `init --global` wires your editors. README install chapter rewritten to keep the two straight
|
|
78
|
+
|
|
79
|
+
## Smarter init (D46 / D47)
|
|
80
|
+
|
|
81
|
+
- Bare `open-memex init` auto-detects installed editors and wires all of them — no more "which client?" for every project
|
|
82
|
+
- Non-strict-JSON configs (JSONC comments, trailing commas): init leaves the file untouched and prints a ready-to-paste snippet instead
|
|
83
|
+
|
|
84
|
+
## uninstall (D48)
|
|
85
|
+
|
|
86
|
+
- New `open-memex uninstall [--client ...] [--global] [--yes]`: removes wiring symmetrically with init, never touches memory data
|
|
87
|
+
|
|
88
|
+
## Empty-file fix (D49)
|
|
89
|
+
|
|
90
|
+
- Empty or whitespace-only config files are treated as blank, not corrupt — init writes into them directly (previously refused with "not valid JSON")
|
|
91
|
+
|
|
92
|
+
## Docs
|
|
93
|
+
|
|
94
|
+
- FAQ merged into the bilingual README (EN + zh-CN); install chapter rewritten; full `init` synopsis documented
|
|
95
|
+
|
|
96
|
+
## [0.4.1] - 2026-09-29
|
|
97
|
+
|
|
98
|
+
- Patch release on the 0.4.0 line (published to npm; no GitHub Release was created at the time).
|
|
99
|
+
|
|
100
|
+
## [0.4.0] - 2026-09-29
|
|
101
|
+
|
|
102
|
+
## Team sync via git
|
|
103
|
+
|
|
104
|
+
- Draft outbox (`appdata project/`) → `sync-status` → `submit` → `promote` / `resolve`: shared memory flows through git branches and PR review, just like code
|
|
105
|
+
- Fast-forward-only pulls, no auto-push — conflicts stop and ask, never overwrite
|
|
106
|
+
- `memory_status` checkpoints: after commits, work chunks, and memory actions
|
|
107
|
+
|
|
108
|
+
## Portable archives
|
|
109
|
+
|
|
110
|
+
- `export` / `import`: `.tar.gz` + `manifest.json`; `visibility: private` excluded by default, `--all` for full migration
|
|
111
|
+
|
|
112
|
+
## Capture loop closed (D42/D43)
|
|
113
|
+
|
|
114
|
+
- §3.5 checkpoint distillation taught in the MCP handshake instructions and `init`-written instruction files: the agent proposes 1–3 distilled captures at checkpoints, nothing saved without approval
|
|
115
|
+
- `distill-agents` output now ends with a "Memory hygiene" section, so opencode users reading AGENTS.md get the checkpoint habit too
|
|
116
|
+
|
|
117
|
+
## Also in this release
|
|
118
|
+
|
|
119
|
+
- 11 memory types (`fact` `preference` `decision` `constraint` `todo` `knowledge` `howto` `gotcha` `lesson` `observation` `reference`)
|
|
120
|
+
- README overhaul, fully bilingual (EN + zh-CN): why open-memex, 11 tools, architecture diagram, stable vs `@alpha` install channels
|
|
121
|
+
- MCP server: 11 tools, re-synced per request; CLI: 26 commands, each with full `--help`
|
|
122
|
+
|
|
123
|
+
Install: `npm install -g open-memex`
|
|
124
|
+
|
|
125
|
+
## [0.3.0] - 2026-09-27
|
|
126
|
+
|
|
127
|
+
- The v2 memory format and protocol ship: `personal` scope (renamed from `user`), RFC 3339 timestamps, type/role split. See `docs/V2-DESIGN.md`; `open-memex migrate --to-v2` converts v1 data. (Published to npm; no GitHub Release was created at the time.)
|
|
128
|
+
|
|
129
|
+
## [0.1.0] - 2026-09-26
|
|
130
|
+
|
|
131
|
+
- First public release under the open-memex name (renamed from my-o-memory): local-first memory for AI coding agents — Markdown files as the source of truth, SQLite FTS5 index, opencode plugin + MCP server + CLI.
|
|
132
|
+
|
|
133
|
+
[Unreleased]: https://github.com/stoneskin/open-memex/compare/v0.6.1...HEAD
|
|
134
|
+
[0.6.1]: https://github.com/stoneskin/open-memex/compare/v0.6.0...v0.6.1
|
|
135
|
+
[0.6.0]: https://github.com/stoneskin/open-memex/compare/v0.5.1...v0.6.0
|
|
136
|
+
[0.5.1]: https://github.com/stoneskin/open-memex/compare/v0.5.0...v0.5.1
|
|
137
|
+
[0.5.0]: https://github.com/stoneskin/open-memex/compare/v0.4.0...v0.5.0
|
|
138
|
+
[0.4.1]: https://github.com/stoneskin/open-memex/compare/v0.4.0...v0.4.1
|
|
139
|
+
[0.4.0]: https://github.com/stoneskin/open-memex/releases/tag/v0.4.0
|
|
140
|
+
[0.3.0]: https://github.com/stoneskin/open-memex/releases/tag/v0.3.0
|
|
141
|
+
[0.1.0]: https://github.com/stoneskin/open-memex/releases/tag/v0.1.0
|
|
142
|
+
|
package/README.md
CHANGED
|
@@ -531,6 +531,8 @@ refresh the installed Agent Skill and instruction files with the latest wording.
|
|
|
531
531
|
If you installed from source or moved the package, `--force` also re-points the
|
|
532
532
|
opencode plugin path.
|
|
533
533
|
|
|
534
|
+
What changed in each version: see the [CHANGELOG](CHANGELOG.md).
|
|
535
|
+
|
|
534
536
|
## Storage layout
|
|
535
537
|
|
|
536
538
|
```
|
|
@@ -597,7 +599,7 @@ open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
|
597
599
|
open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]
|
|
598
600
|
open-memex config # print effective config
|
|
599
601
|
open-memex config set <key> <value> # change a setting
|
|
600
|
-
open-memex doctor # environment health check
|
|
602
|
+
open-memex doctor # environment health check (incl. plugin entry)
|
|
601
603
|
open-memex audit # memory health check (duplicates, stale, broken chains)
|
|
602
604
|
open-memex capture --dry-run "记住我喜欢简洁的回答" # preview keyword capture
|
|
603
605
|
open-memex mcp --print-config vscode|cursor|claude|opencode|visualstudio
|
package/README.zh-CN.md
CHANGED
|
@@ -365,6 +365,8 @@ npm install -g open-memex@latest # 或 @alpha
|
|
|
365
365
|
|
|
366
366
|
编辑器配置指向已安装的 `open-memex` 命令,所以升级不需要重新接线。大版本升级后跑一次 `open-memex init --force`,把已装的 Agent Skill 和指令文件刷新到最新文案。如果你是从源码安装或挪过包的位置,`--force` 也会把 opencode 插件路径重新指好。
|
|
367
367
|
|
|
368
|
+
每个版本具体变了什么:见 [CHANGELOG](CHANGELOG.md)(英文)。
|
|
369
|
+
|
|
368
370
|
## 存储布局
|
|
369
371
|
|
|
370
372
|
```
|
|
@@ -419,7 +421,7 @@ open-memex init [--client vscode|cursor|opencode|visualstudio]
|
|
|
419
421
|
open-memex uninstall [--client vscode|cursor|opencode|visualstudio] [--global] [--yes]
|
|
420
422
|
open-memex config # 打印生效配置
|
|
421
423
|
open-memex config set <key> <value> # 改一个设置
|
|
422
|
-
open-memex doctor #
|
|
424
|
+
open-memex doctor # 环境健康检查(含插件入口巡检)
|
|
423
425
|
open-memex audit # 记忆健康检查(近重复、陈旧、断链)
|
|
424
426
|
open-memex capture --dry-run "记住我喜欢简洁的回答" # 预览关键词捕获
|
|
425
427
|
open-memex mcp --print-config vscode|cursor|claude|opencode|visualstudio
|
package/dist/doctor.js
CHANGED
|
@@ -81,6 +81,135 @@ function lockingCheck() {
|
|
|
81
81
|
return { name: "locking", ok: false, detail: String(err) };
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* D59: patrol the opencode native-plugin entry. The D58 field failure was
|
|
86
|
+
* silent — the entry was configured, the host never loaded it, and nothing
|
|
87
|
+
* anywhere said why. Two death modes to catch:
|
|
88
|
+
* 1. the entry's target file does not exist (package moved, nvm rolled
|
|
89
|
+
* to a new version dir, checkout deleted);
|
|
90
|
+
* 2. the plugin's import closure runtime-imports @opencode-ai/plugin
|
|
91
|
+
* (or another host-only module the package does not depend on),
|
|
92
|
+
* which is unresolvable from a global install (D58 regression).
|
|
93
|
+
* Read-only; only inspects the user-level opencode configs.
|
|
94
|
+
*/
|
|
95
|
+
function opencodePluginCheck() {
|
|
96
|
+
const name = "opencode plugin";
|
|
97
|
+
try {
|
|
98
|
+
const jsonPath = opencodeGlobalConfigPath();
|
|
99
|
+
const candidates = [jsonPath, jsonPath.replace(/\.json$/, ".jsonc")];
|
|
100
|
+
const entries = [];
|
|
101
|
+
let sawConfig = false;
|
|
102
|
+
for (const f of candidates) {
|
|
103
|
+
if (!fs.existsSync(f))
|
|
104
|
+
continue;
|
|
105
|
+
sawConfig = true;
|
|
106
|
+
const text = fs.readFileSync(f, "utf8");
|
|
107
|
+
try {
|
|
108
|
+
const doc = JSON.parse(text);
|
|
109
|
+
const list = Array.isArray(doc.plugin) ? doc.plugin : [];
|
|
110
|
+
for (const e of list) {
|
|
111
|
+
if (typeof e === "string")
|
|
112
|
+
entries.push(e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// JSONC (comments/trailing commas): fall back to scanning quoted
|
|
117
|
+
// strings for plugin entries.
|
|
118
|
+
for (const m of text.matchAll(/"(file:[^"]+|[^"]+\.(?:ts|js|mts|mjs))"/g)) {
|
|
119
|
+
entries.push(m[1]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!sawConfig) {
|
|
124
|
+
return { name, ok: true, detail: "no global opencode config found (skipped)" };
|
|
125
|
+
}
|
|
126
|
+
const problems = [];
|
|
127
|
+
const RUNTIME_SDK_IMPORT = /^\s*import\s+(?!type\b)[^;]*?["']@opencode-ai\/plugin(?:\/[^"']*)?["']/m;
|
|
128
|
+
const RUNTIME_SDK_REQUIRE = /require\(\s*["']@opencode-ai\/plugin(?:\/[^"']*)?["']\s*\)/;
|
|
129
|
+
// An entry is ours when its path names the package, or when the target
|
|
130
|
+
// file carries the plugin's "[open-memex]" log marker (source checkouts
|
|
131
|
+
// can live at any path).
|
|
132
|
+
const isOurEntry = (entry) => {
|
|
133
|
+
if (entry.includes("open-memex"))
|
|
134
|
+
return true;
|
|
135
|
+
try {
|
|
136
|
+
const target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
137
|
+
if (!fs.existsSync(target))
|
|
138
|
+
return false;
|
|
139
|
+
return fs.readFileSync(target, "utf8").slice(0, 65536).includes("[open-memex]");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const scanSdkImports = (entryFile) => {
|
|
146
|
+
// Walk the entry's relative-import closure looking for host-SDK
|
|
147
|
+
// runtime imports (type-only imports are erased and fine).
|
|
148
|
+
const offenders = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const queue = [entryFile];
|
|
151
|
+
while (queue.length > 0 && seen.size < 300) {
|
|
152
|
+
const file = queue.shift();
|
|
153
|
+
if (seen.has(file))
|
|
154
|
+
continue;
|
|
155
|
+
seen.add(file);
|
|
156
|
+
let src;
|
|
157
|
+
try {
|
|
158
|
+
src = fs.readFileSync(file, "utf8");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (RUNTIME_SDK_IMPORT.test(src) || RUNTIME_SDK_REQUIRE.test(src)) {
|
|
164
|
+
offenders.push(file);
|
|
165
|
+
}
|
|
166
|
+
for (const m of src.matchAll(/(?:from\s+|import\s*\(\s*|import\s+)["'](\.[^"']+)["']/g)) {
|
|
167
|
+
const spec = m[1];
|
|
168
|
+
const base = path.resolve(path.dirname(file), spec);
|
|
169
|
+
for (const cand of [base, `${base}.ts`, `${base}.js`, path.join(base, "index.ts")]) {
|
|
170
|
+
if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
|
|
171
|
+
queue.push(cand);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return offenders;
|
|
178
|
+
};
|
|
179
|
+
const ours = entries.filter(isOurEntry);
|
|
180
|
+
if (ours.length === 0) {
|
|
181
|
+
return { name, ok: true, detail: "no global plugin entry (per-project or MCP wiring is not checked)" };
|
|
182
|
+
}
|
|
183
|
+
for (const entry of ours) {
|
|
184
|
+
let target;
|
|
185
|
+
try {
|
|
186
|
+
target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
problems.push(`entry "${entry}" is not a parseable file URL`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!fs.existsSync(target)) {
|
|
193
|
+
problems.push(`entry points at ${target}, which does not exist — run \`open-memex init --client opencode --global --force\` to re-point it`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
for (const f of scanSdkImports(target)) {
|
|
197
|
+
problems.push(`${f} runtime-imports @opencode-ai/plugin — opencode silently skips plugins whose imports it cannot resolve from a global install (D58)`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (problems.length > 0) {
|
|
201
|
+
return { name, ok: false, detail: problems.join("; ") };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
name,
|
|
205
|
+
ok: true,
|
|
206
|
+
detail: `${ours.length} plugin ${ours.length === 1 ? "entry" : "entries"}: target exists, no host-SDK runtime imports`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
return { name, ok: false, detail: String(err) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
84
213
|
/** Spawn the real MCP server, handshake, and verify the five tools list. */
|
|
85
214
|
function mcpCheck() {
|
|
86
215
|
const name = "mcp";
|
|
@@ -299,7 +428,7 @@ function vscodeMcpCheck() {
|
|
|
299
428
|
}
|
|
300
429
|
export async function runDoctor() {
|
|
301
430
|
console.log("open-memex doctor");
|
|
302
|
-
const checks = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), vscodeMcpCheck()];
|
|
431
|
+
const checks = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
|
|
303
432
|
checks.push(await mcpCheck());
|
|
304
433
|
checks.push(legacyCheck());
|
|
305
434
|
let allOk = true;
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -1197,13 +1197,26 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
1197
1197
|
register. Rule going forward: plugin code imports host SDK types only;
|
|
1198
1198
|
runtime imports must resolve from the package's own dependencies.
|
|
1199
1199
|
|
|
1200
|
+
- **D59** — `doctor` patrols the plugin entry it used to take on faith
|
|
1201
|
+
(0.6.2-alpha.1). D58's failure was invisible to every existing check:
|
|
1202
|
+
`doctor` verified the MCP server end-to-end but never looked at the
|
|
1203
|
+
opencode plugin entry, so a configured-but-dead plugin passed all
|
|
1204
|
+
checks. The new `opencode plugin` check reads the user-level opencode
|
|
1205
|
+
configs (`opencode.json` + `opencode.jsonc`), attributes entries to
|
|
1206
|
+
this plugin by package path or the `[open-memex]` marker (source
|
|
1207
|
+
checkouts live anywhere), and fails when (a) the entry's target file
|
|
1208
|
+
does not exist — with the `init --force` re-point command named — or
|
|
1209
|
+
(b) the entry's relative-import closure runtime-imports
|
|
1210
|
+
`@opencode-ai/plugin`, the exact D58 death. It checks only what can be
|
|
1211
|
+
attributed; per-project and MCP wiring are out of scope.
|
|
1212
|
+
|
|
1200
1213
|
## Open Questions
|
|
1201
1214
|
|
|
1202
1215
|
_All resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull)._
|
|
1203
1216
|
|
|
1204
1217
|
- **Checkpoint proactivity for capture (open, 2026-10-02):** D53 retired checkpoint *polling* — the server pushes draft counts instead. But agent proactivity at task checkpoints genuinely helps knowledge capture (§3.5 distillation), so the open question is the degree: proactive enough to catch distillable moments, without nagging the user or burning tokens at every checkpoint. Candidate directions (server-pushed hints in tool results, client-side heuristics, or a middle setting) to be evaluated in a later version; default posture TBD.
|
|
1205
1218
|
|
|
1206
|
-
- **Retrieval robustness: phrasing, synonyms, extra words (open, 2026-10-03):** keyword search matches word forms, not meaning — the asker phrases it differently, adds a word, or uses a synonym and the memory is missed. The current FTS5 setup (porter stemming + pre-tokenized CJK bigrams) already covers morphology and substrings; it cannot bridge semantics. Layered candidate directions, cheapest first: (1) query construction — never feed a raw sentence to FTS; extract content terms, drop stopwords, OR them and let bm25 rank, so extra words hurt ranking instead of killing the match; (2) agent-side query expansion as an explicit protocol in the skill/instructions (decompose the question into concepts, generate 2–3 synonym variants per concept, run several searches before giving up — the LLM is the synonym dictionary, no embedding model needed); (3) capture-time aliasing — when a memory is written, the agent attaches keyword/synonym aliases so future phrasings can hit it. Embeddings are the design's reserved optional capability: real semantic matching, but they require a design decision and a local model to distribute, so they stay deferred unless (1)–(3) prove insufficient. Note (3) pairs naturally with the proactive-capture question below: the same capture pass that proposes a memory can attach its aliases.
|
|
1219
|
+
- **Retrieval robustness: phrasing, synonyms, extra words (open, 2026-10-03):** keyword search matches word forms, not meaning — the asker phrases it differently, adds a word, or uses a synonym and the memory is missed. The current FTS5 setup (porter stemming + pre-tokenized CJK bigrams) already covers morphology and substrings; it cannot bridge semantics. Layered candidate directions, cheapest first: (1) query construction — never feed a raw sentence to FTS; extract content terms, drop stopwords, OR them and let bm25 rank, so extra words hurt ranking instead of killing the match; (2) agent-side query expansion as an explicit protocol in the skill/instructions (decompose the question into concepts, generate 2–3 synonym variants per concept, run several searches before giving up — the LLM is the synonym dictionary, no embedding model needed); (3) capture-time aliasing — when a memory is written, the agent attaches keyword/synonym aliases so future phrasings can hit it. Embeddings are the design's reserved optional capability: real semantic matching, but they require a design decision and a local model to distribute, so they stay deferred unless (1)–(3) prove insufficient. Note (3) pairs naturally with the proactive-capture question below: the same capture pass that proposes a memory can attach its aliases. **Status (2026-10-03): layer (1) shipped (D56, 0.6.1)** — query construction now filters EN/CJK function words, dedupes, and caps terms. Layers (2) agent-side expansion and (3) capture-time aliasing remain open; embeddings stay deferred.
|
|
1207
1220
|
|
|
1208
1221
|
- **Proactive capture: agent judgment of what is worth remembering (open, 2026-10-03):** companion to the checkpoint-proactivity question above — that one asks *when* to prompt; this one asks *what qualifies*. Agent hosts with good long-term memory (e.g. Muse) get their results from explicit memorability criteria plus checkpoint consolidation, not from silent background magic; open-memex can adopt both halves. Candidate criteria to teach via the skill/instructions: decisions **and their rationale**, preferences, conventions, gotchas, and abandoned paths ("we tried X, it failed because…") are worth proposing; one-off task details and anything re-derivable from the code are not. Two postures to evaluate: propose-at-checkpoint (agent asks "worth saving?", user vetoes, then `memory_add`) versus auto-draft (agent writes outbox drafts unasked and surfaces them in a batch at the next checkpoint — drafts are git-invisible and deletable, so the cost of a wrong guess is low). Hard boundaries either way: project scope never auto-submits, and no cross-session background consolidation is possible — the plugin lives inside sessions, so consolidation happens at in-session checkpoints and wrap-up passes only. This question also decides how much aliasing (retrieval question above) the capture pass should generate.
|
|
1209
1222
|
|
|
@@ -1211,11 +1224,11 @@ _All resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull)._
|
|
|
1211
1224
|
|
|
1212
1225
|
- **Retrospective distillation: mining host transcripts after the fact (open, 2026-10-03):** open-memex captures only in-session (keyword triggers, explicit `memory_add`) and keeps **no transcript** — so anything not captured during the conversation is gone once it ends. Hosts with durable memory (e.g. Muse) avoid this loss with two layers open-memex skipped: full transcript retention plus background distillation. The gap is architectural, not a timing detail: the raw material for a second chance does not exist on our side. But the hosts themselves already retain it — opencode persists sessions and messages in its local store; Claude Code keeps JSONL transcripts under `~/.claude/projects/`; VS Code Copilot's chat state is locked in workspace storage (readable, fragile, not a first target). Candidate direction: a "replay distillation" flow that exports a past session's transcript and has an agent mine it in a dedicated review session, with every extracted candidate landing as an **outbox draft** for human review — never a direct save. Boundaries that keep this inside the existing design: open-memex builds no transcript store of its own (borrow the host's), mining judgment is the agent's LLM work (we provide the export and the queue, not an extractor), fully local, mined drafts carry their provenance (`source` marks them as distilled, from which session). Open sub-questions: host adapters per tool (opencode store format, Claude Code JSONL), how the user picks sessions to mine (by date? by project? "everything since last sync"?), and whether the proactive-capture criteria (open question above) should be the mining rubric verbatim — likely yes.
|
|
1213
1226
|
|
|
1214
|
-
- **Retrieval explainability and memory health (open, 2026-10-03):** from a heavy-user review. Search is currently a black box: when a memory is missed, there is no way to tell whether the query was bad, the memory is stale, it was ranked down by review state, or it is hiding behind a supersede chain. Candidate additions: a debug/explain view for `search` (matched terms, bm25 score, ranking effects, supersede exclusions), and a memory-side audit next to `doctor` (which today checks the environment, not the memories): near-duplicate candidates, memories untouched for N days still occupying injection slots, broken supersede chains, orphan scopes. Also undocumented and untested territory the same review surfaced: **concurrent sessions** — several agents on one machine writing the same store (simultaneous `memory_add`, reindex races, submit vs pull). File-locking/WAL behavior needs to be established, documented, and ideally guarded by a doctor check before a user finds out via a corrupt index.
|
|
1227
|
+
- **Retrieval explainability and memory health (open, 2026-10-03):** from a heavy-user review. Search is currently a black box: when a memory is missed, there is no way to tell whether the query was bad, the memory is stale, it was ranked down by review state, or it is hiding behind a supersede chain. Candidate additions: a debug/explain view for `search` (matched terms, bm25 score, ranking effects, supersede exclusions), and a memory-side audit next to `doctor` (which today checks the environment, not the memories): near-duplicate candidates, memories untouched for N days still occupying injection slots, broken supersede chains, orphan scopes. Also undocumented and untested territory the same review surfaced: **concurrent sessions** — several agents on one machine writing the same store (simultaneous `memory_add`, reindex races, submit vs pull). File-locking/WAL behavior needs to be established, documented, and ideally guarded by a doctor check before a user finds out via a corrupt index. **Status: resolved (D57, 0.6.1)** — `search --explain`, the `audit` command, and the concurrency posture (WAL + unified 5s busy timeout, tmp+rename writes, doctor locking check) all shipped.
|
|
1215
1228
|
|
|
1216
|
-
- **Host parity: plugin vs MCP tool surface (open, 2026-10-03):** the opencode plugin exposes 5 memory tools; the MCP server exposes 11 (`memory_status`, submit/propose/promote/resolve/pr_status missing on the plugin side). A user switching hosts mid-week hits missing tools with no explanation and reasonably assumes breakage. Options: bring the plugin to parity, or document the asymmetry as intentional (CLI covers the rest) in the tool descriptions themselves so the absence reads as design rather than defect. Needs a decision; silence is the current — worst — option.
|
|
1229
|
+
- **Host parity: plugin vs MCP tool surface (open, 2026-10-03):** the opencode plugin exposes 5 memory tools; the MCP server exposes 11 (`memory_status`, submit/propose/promote/resolve/pr_status missing on the plugin side). A user switching hosts mid-week hits missing tools with no explanation and reasonably assumes breakage. Options: bring the plugin to parity, or document the asymmetry as intentional (CLI covers the rest) in the tool descriptions themselves so the absence reads as design rather than defect. Needs a decision; silence is the current — worst — option. **Status: decided (D56, 0.6.1)** — the asymmetry is documented in the MCP-only tool descriptions with CLI equivalents named; full plugin parity remains a possible future feature, no longer an open question.
|
|
1217
1230
|
|
|
1218
|
-
- **Novice experience: first success and plain language (open, 2026-10-03):** from an AI-novice review. The install ends with instructions, not an experience: candidate fix is a 60-second magic moment — init offers to save one sample memory and immediately has the user's agent answer a question using it, so the user *sees* recall work before being asked to trust it. Same screen should state the privacy model in plain words (personal never leaves this machine, no cloud, detected secrets are masked — all already true, all currently buried in docs), and the first time a suspected secret is masked, the capture path should say so. Underneath: a plain-language layer over the state machine ("saved on this computer" / "shared with the team") with outbox/submit/promote vocabulary becoming the advanced view — this is the same vocabulary wall the PM/BA question above hits from the other side.
|
|
1231
|
+
- **Novice experience: first success and plain language (open, 2026-10-03):** from an AI-novice review. The install ends with instructions, not an experience: candidate fix is a 60-second magic moment — init offers to save one sample memory and immediately has the user's agent answer a question using it, so the user *sees* recall work before being asked to trust it. Same screen should state the privacy model in plain words (personal never leaves this machine, no cloud, detected secrets are masked — all already true, all currently buried in docs), and the first time a suspected secret is masked, the capture path should say so. Underneath: a plain-language layer over the state machine ("saved on this computer" / "shared with the team") with outbox/submit/promote vocabulary becoming the advanced view — this is the same vocabulary wall the PM/BA question above hits from the other side. **Status (2026-10-03): partially addressed** — `init` now states the privacy model in plain words (D56); the 60-second magic moment and the plain-language layer remain open.
|
|
1219
1232
|
|
|
1220
1233
|
- **Memory visibility and correctability (open, 2026-10-03):** also from the novice review, and arguably the trust foundation for every other feature. A non-CLI user currently cannot answer "what does my assistant remember about me?" without reading Markdown files. Candidate directions: a human-readable inventory (local HTML export or equivalent) listing memories in plain language with provenance shown the way a person asks for it ("saved 3 days ago while you were discussing deploys in project X") and one-step forget/undo from the same view. Provenance fields already exist in frontmatter; this is presentation, not schema. Without it, proactive capture (above) will read to novices as surveillance rather than help — so this question probably gates the auto-draft posture decision in the capture question.
|
|
1221
1234
|
|
package/package.json
CHANGED
package/src/doctor.ts
CHANGED
|
@@ -93,6 +93,134 @@ function lockingCheck(): Check {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* D59: patrol the opencode native-plugin entry. The D58 field failure was
|
|
98
|
+
* silent — the entry was configured, the host never loaded it, and nothing
|
|
99
|
+
* anywhere said why. Two death modes to catch:
|
|
100
|
+
* 1. the entry's target file does not exist (package moved, nvm rolled
|
|
101
|
+
* to a new version dir, checkout deleted);
|
|
102
|
+
* 2. the plugin's import closure runtime-imports @opencode-ai/plugin
|
|
103
|
+
* (or another host-only module the package does not depend on),
|
|
104
|
+
* which is unresolvable from a global install (D58 regression).
|
|
105
|
+
* Read-only; only inspects the user-level opencode configs.
|
|
106
|
+
*/
|
|
107
|
+
function opencodePluginCheck(): Check {
|
|
108
|
+
const name = "opencode plugin";
|
|
109
|
+
try {
|
|
110
|
+
const jsonPath = opencodeGlobalConfigPath();
|
|
111
|
+
const candidates = [jsonPath, jsonPath.replace(/\.json$/, ".jsonc")];
|
|
112
|
+
const entries: string[] = [];
|
|
113
|
+
let sawConfig = false;
|
|
114
|
+
for (const f of candidates) {
|
|
115
|
+
if (!fs.existsSync(f)) continue;
|
|
116
|
+
sawConfig = true;
|
|
117
|
+
const text = fs.readFileSync(f, "utf8");
|
|
118
|
+
try {
|
|
119
|
+
const doc = JSON.parse(text) as { plugin?: unknown };
|
|
120
|
+
const list = Array.isArray(doc.plugin) ? doc.plugin : [];
|
|
121
|
+
for (const e of list) {
|
|
122
|
+
if (typeof e === "string") entries.push(e);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// JSONC (comments/trailing commas): fall back to scanning quoted
|
|
126
|
+
// strings for plugin entries.
|
|
127
|
+
for (const m of text.matchAll(/"(file:[^"]+|[^"]+\.(?:ts|js|mts|mjs))"/g)) {
|
|
128
|
+
entries.push(m[1]!);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!sawConfig) {
|
|
133
|
+
return { name, ok: true, detail: "no global opencode config found (skipped)" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const problems: string[] = [];
|
|
137
|
+
const RUNTIME_SDK_IMPORT =
|
|
138
|
+
/^\s*import\s+(?!type\b)[^;]*?["']@opencode-ai\/plugin(?:\/[^"']*)?["']/m;
|
|
139
|
+
const RUNTIME_SDK_REQUIRE = /require\(\s*["']@opencode-ai\/plugin(?:\/[^"']*)?["']\s*\)/;
|
|
140
|
+
|
|
141
|
+
// An entry is ours when its path names the package, or when the target
|
|
142
|
+
// file carries the plugin's "[open-memex]" log marker (source checkouts
|
|
143
|
+
// can live at any path).
|
|
144
|
+
const isOurEntry = (entry: string): boolean => {
|
|
145
|
+
if (entry.includes("open-memex")) return true;
|
|
146
|
+
try {
|
|
147
|
+
const target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
148
|
+
if (!fs.existsSync(target)) return false;
|
|
149
|
+
return fs.readFileSync(target, "utf8").slice(0, 65536).includes("[open-memex]");
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const scanSdkImports = (entryFile: string): string[] => {
|
|
155
|
+
// Walk the entry's relative-import closure looking for host-SDK
|
|
156
|
+
// runtime imports (type-only imports are erased and fine).
|
|
157
|
+
const offenders: string[] = [];
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
const queue = [entryFile];
|
|
160
|
+
while (queue.length > 0 && seen.size < 300) {
|
|
161
|
+
const file = queue.shift()!;
|
|
162
|
+
if (seen.has(file)) continue;
|
|
163
|
+
seen.add(file);
|
|
164
|
+
let src: string;
|
|
165
|
+
try {
|
|
166
|
+
src = fs.readFileSync(file, "utf8");
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (RUNTIME_SDK_IMPORT.test(src) || RUNTIME_SDK_REQUIRE.test(src)) {
|
|
171
|
+
offenders.push(file);
|
|
172
|
+
}
|
|
173
|
+
for (const m of src.matchAll(/(?:from\s+|import\s*\(\s*|import\s+)["'](\.[^"']+)["']/g)) {
|
|
174
|
+
const spec = m[1]!;
|
|
175
|
+
const base = path.resolve(path.dirname(file), spec);
|
|
176
|
+
for (const cand of [base, `${base}.ts`, `${base}.js`, path.join(base, "index.ts")]) {
|
|
177
|
+
if (fs.existsSync(cand) && fs.statSync(cand).isFile()) {
|
|
178
|
+
queue.push(cand);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return offenders;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const ours = entries.filter(isOurEntry);
|
|
188
|
+
if (ours.length === 0) {
|
|
189
|
+
return { name, ok: true, detail: "no global plugin entry (per-project or MCP wiring is not checked)" };
|
|
190
|
+
}
|
|
191
|
+
for (const entry of ours) {
|
|
192
|
+
let target: string;
|
|
193
|
+
try {
|
|
194
|
+
target = entry.startsWith("file:") ? fileURLToPath(entry) : entry;
|
|
195
|
+
} catch {
|
|
196
|
+
problems.push(`entry "${entry}" is not a parseable file URL`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!fs.existsSync(target)) {
|
|
200
|
+
problems.push(
|
|
201
|
+
`entry points at ${target}, which does not exist — run \`open-memex init --client opencode --global --force\` to re-point it`,
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
for (const f of scanSdkImports(target)) {
|
|
206
|
+
problems.push(
|
|
207
|
+
`${f} runtime-imports @opencode-ai/plugin — opencode silently skips plugins whose imports it cannot resolve from a global install (D58)`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (problems.length > 0) {
|
|
212
|
+
return { name, ok: false, detail: problems.join("; ") };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name,
|
|
216
|
+
ok: true,
|
|
217
|
+
detail: `${ours.length} plugin ${ours.length === 1 ? "entry" : "entries"}: target exists, no host-SDK runtime imports`,
|
|
218
|
+
};
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return { name, ok: false, detail: String(err) };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
96
224
|
/** Spawn the real MCP server, handshake, and verify the five tools list. */
|
|
97
225
|
function mcpCheck(): Promise<Check> {
|
|
98
226
|
const name = "mcp";
|
|
@@ -320,7 +448,7 @@ function vscodeMcpCheck(): Check {
|
|
|
320
448
|
|
|
321
449
|
export async function runDoctor(): Promise<boolean> {
|
|
322
450
|
console.log("open-memex doctor");
|
|
323
|
-
const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), vscodeMcpCheck()];
|
|
451
|
+
const checks: Check[] = [nodeCheck(), configCheck(), scopeCheck(), storageCheck(), lockingCheck(), opencodePluginCheck(), vscodeMcpCheck()];
|
|
324
452
|
checks.push(await mcpCheck());
|
|
325
453
|
checks.push(legacyCheck());
|
|
326
454
|
let allOk = true;
|