open-memex
Version:
Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.
1,616 lines • 122 kB
Markdown
# OpenMemex — Design Document (protocol v0.2)
**Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D74 are settled; open questions are tracked at the end of this document.
**Author:** Stone, with 小沐
**Changelog vs v1:** incorporates round-3 review from Perplexity, Grok, Gemini, ChatGPT, DeepSeek.
Key changes: Design Principles section; `role` separated from `type`; two iron rules;
scope/visibility/provenance/confidence split; ISO 8601 times + `schema_version`;
bidirectional supersede chain; `importance` replaces numeric priority; explicit pull;
versioned-append sync (no silent LWW); embeddings as optional capability; v1→v2 migration;
Decisions log; Prior Art; solo-dev adoption path.
> Note: "protocol v0.2" versions the **protocol**, not the plugin. The protocol will outlive any
> single implementation.
---
## Design Principles
1. **Memory is data by default, never instructions by default.** A memory only becomes an instruction
through an explicit, reviewable gate (§3.3).
2. **Local is always the source of personal truth.** The `personal` scope never syncs, never uploads.
3. **Markdown is the source of truth; every index is rebuildable.** Lose the DB, rebuild from files.
4. **Nothing leaves the machine without explicit user action.** Promotion and sync are always opt-in.
5. **Core must never silently resolve semantic memory conflicts.** Surface them; let humans decide.
6. **Adapters translate; they never implement memory logic.** All memory logic lives in Core.
7. **Memory is the entrance of knowledge, not its final form.** Terminal states are docs / ADRs /
AGENTS.md instructions — memory is how knowledge gets captured and found.
8. **Distill conversations; do not archive them.** The default unit of memory is the useful
conclusion from a conversation, not the full transcript: what was learned, how it was resolved,
and where the authoritative source lives.
9. **Portable second brain, not a walled garden.** Long-form personal notes are valid memories when
the user explicitly saves them; Markdown keeps them readable, exportable, and movable to other
tools or machines.
---
## 1. Vision & Positioning
**OpenMemex is an open, local-first memory layer and interoperable protocol for AI coding agents.**
- **Tagline:** *"Capture knowledge once, make it available to every AI agent."*
- **Local-first, open source (Apache-2.0).** No cloud SaaS, no account, no mandatory network calls.
- **The pain it kills:** every developer's AI learns in isolation. Dev A spends three days debugging an
environment quirk with AI help; dev B rediscovers it next week. Hard-won knowledge should flow
**personal → team → organization** instead of evaporating with each session.
- **MCP is an interface, not the identity.** MCP / CLI / REST / SDK are access layers over the protocol,
so the project is never locked to one transport or one agent tool (opencode, VS Code Copilot, Cursor,
Claude Code, Windsurf, …).
- **Second-brain lens:** for individuals, OpenMemex can also be a local Markdown second brain for
AI-assisted work — similar in spirit to users asking AI to save notes into Obsidian or Notion, but
with agent recall, scopes, review, and redaction built in from the start.
- **Company lens:** at organizational scale the same pain is tribal knowledge — senior engineers'
hard-won experience evaporates when they move on, and every incident gets re-debugged by someone
new. The current phase therefore prioritizes *capture*: valuable knowledge must land in memory
first, because team/org sharing, onboarding, and incident learning all build on that foundation.
(No capture, nothing to inherit.)
### Non-goals
- Not a cloud SaaS. Not an account system.
- Not a replacement for documentation (§12).
- No automatic exfiltration (§4).
---
## 2. Architecture: Core / Interfaces / Providers
```
┌──────────────────────────────────────────────────────────┐
│ MEMORY CORE (pure TS) │
│ store markdown source of truth + rebuildable index │
│ retrieve hybrid search · rerank · conflict resolution │
│ injection policy │
│ capture tools · keyword triggers · implicit (opt-in) │
│ sync git transport · merge · pull/push/status │
│ trust redaction · admission barrier · audit │
└──────────────────────────────────────────────────────────┘
│ Interfaces (thin adapters, no memory logic)
├─ MCP server ........... primary cross-tool interface
├─ opencode plugin ...... v1 plugin, thinned to an adapter
├─ CLI .................. management, sync, promotion, resolve
└─ (future) REST / SDK
│ Providers (storage backends, swappable)
├─ LocalProvider ........ default; SQLite + files on disk
├─ GitProvider .......... repo-synced shared scopes
└─ RemoteProvider ....... HTTP; remote-server sample in examples/
```
**Provider capability model** — every provider declares what it can do:
```ts
interface MemoryProviderCapabilities {
read: boolean; write: boolean; delete: boolean;
history: boolean; // version / audit trail
sync: "none" | "pull" | "push" | "bidirectional";
}
```
Core never knows about AWS/GCP/any-cloud. A future cloud deployment is *a custom `RemoteProvider`*,
not a core change. The auth layer of `RemoteProvider` is pluggable (token for the sample;
OIDC/SAML for enterprise later) so the sync protocol never needs rework for enterprise auth.
---
## 3. Data Model
One memory = one file. `id` = filename = ULID. No other ID scheme.
```yaml
id: 01K6AB3XZQ7WVD9J1M2N4P5Q6R7
schema_version: 2
scope: project # personal | project | org (ownership: WHO owns it)
# team, public → schema enum REJECTS writes (reserved)
scope_key: project__my-repo__9f2c1a4b8e3d # storage identity (repo-derived, or "personal")
visibility: internal # private | internal | shared (read access: WHO may read)
project_name: my-repo
type: decision # content kind: what the memory IS (§3.2)
role: knowledge # knowledge | instruction — how it may be USED (§3.3)
importance: normal # low | normal | high (ranking hint; replaces 1–10 priority)
status: active # active | superseded | deprecated | retracted | archived
tags: [auth, oauth]
aliases: ["oauth login flow"] # optional: capture-time phrasing variants (D61)
source: user # provenance: user | tool | keyword | inference | import
created_at: "2026-09-26T12:00:00Z" # RFC 3339, never bare epoch
updated_at: "2026-09-26T12:00:00Z"
supersedes: null # on the NEW memory → points BACK to what it replaces
superseded_by: null # on the OLD memory → points FORWARD to its replacement
review_state: approved # draft | proposed | approved | rejected | published (promotion)
proposed_by: stoneskin
approved_by: stoneskin
derived_from: null # for propose-copies: the personal memory this came from
review_note: null
review_history: [] # append-only [{at, by, from, to, note}] audit trail
```
Field set as implemented (`src/store/markdown.ts`). Earlier drafts of this
example listed aspirational fields (`revision`, `trust_level`, `confidence`,
`via`, `expires_at`, `paths`, `related`, `canonical_ref`) that never shipped;
they are not part of protocol v0.2.
### 3.1 Type taxonomy (content kind)
`fact` `preference` `decision` `constraint` `todo` `knowledge` `howto` `gotcha`
`lesson` `observation` `reference`
`type` describes **what the content is** — single-valued, and it drives behavior
(lifecycle, review, rendering, retrieval). `role` describes **how it may be used**.
A `decision` with `role: knowledge` is retrievable history. Only `role: instruction` may enter
instruction context. (Separation adopted from review: mixing usage semantics into `type` was a design smell.)
One-line definitions:
- `fact` — a verifiable atomic statement (timezone, version number, path, account).
- `preference` — user likes, dislikes, working style.
- `decision` — a choice made + why; participates in supersede chains.
- `constraint` — a hard rule that must not be violated (release process, permission boundaries).
- `todo` — an actionable item with completion state.
- `knowledge` — declarative knowledge (how a system works, concept explanations).
- `howto` — steps to accomplish X.
- `gotcha` — a pitfall: don't do X because Y.
- `lesson` — a takeaway from experience, including incident postmortems (the incident id goes in `tags`).
- `observation` — noticed but not yet distilled.
- `reference` — a pointer to the authoritative doc via `canonical_ref`; the memory holds the summary.
A type earns its place only if the system treats it differently. If two candidates
share lifecycle, retrieval, and rendering, the loser becomes a tag. (D41 merged
`warning`→`gotcha`, `workflow`→`howto`, `incident`→`lesson`, `architecture`→`knowledge`.)
`tags` are retrieval hints, not a second type system: `type` says what the memory is, while tags say
which topics, tools, subsystems, paths, or incidents it relates to. Write paths should preserve
human-provided tags and may suggest simple normalized tags (for example `vscode`, `mcp`, `windows`,
`auth`, `onboarding`) to improve search without changing memory semantics.
### 3.2 Iron rules
- **Iron rule 1 — the instruction gate:** only memories with `role: instruction`
AND `instruction_state: approved` AND `trust_level: trusted|reviewed` may enter an agent's
instruction context. `source: inference` memories can NEVER auto-enter — they require human review.
Org-level instructions additionally require a trusted owner. No privilege escalation: a `personal`
memory can never become an instruction for anyone but its owner.
- **Iron rule 2 — personal scope guard:** a `personal`-scope memory with `role: instruction` must be
explicitly marked, and `open-memex distill` must NEVER recommend it for AGENTS.md. (Prevents
"I hate ESLint in this project" from becoming team policy.)
### 3.3 Lifecycle
`active → superseded | deprecated | retracted | archived`
- `superseded`: replaced by a newer memory; bidirectional chain (`supersedes` / `superseded_by`).
Retrieval returns only the newest of a chain.
- **Chain integrity:** Core validates that `supersedes`/`superseded_by` are pairwise consistent on every
read. If one side is missing (e.g. hand-edited markdown), Core auto-completes it and logs a warning —
a broken chain must never silently degrade retrieval.
- `deprecated`: known-invalid; kept as a warning ("don't do X anymore").
- `retracted`: withdrawn as wrong; excluded from retrieval, kept for audit.
- `archived`: out of scope; excluded from retrieval.
- `review_state` (promotion workflow) is orthogonal to `status` (freshness).
### 3.4 Dedup on write
Content hash + fuzzy match against existing memories. A superseding write does **not** overwrite:
the old memory becomes `status: superseded` with `superseded_by` pointing forward. History preserved;
queries rank `active` first.
### 3.5 Conversation distillation
OpenMemex does **not** store raw AI chat transcripts by default. A captured memory should usually be
a short, human-approved artifact distilled from the conversation: the final conclusion, important
facts, why the answer matters later, and how the issue was resolved. Good distilled memory candidates
answer some combination of:
- What did we learn or decide?
- What solved the problem, including key commands, files, links, or steps?
- What mistake or gotcha should the next developer avoid?
- Which scope owns it: `personal`, `project`, or future `org`?
- If the information already exists in project docs, where is the authoritative doc?
If stable knowledge already lives in a project document, prefer a `type: reference` memory with
`canonical_ref` pointing at that document over duplicating the full content. Memory should help the
agent find and apply authoritative docs, not become an outdated second copy of them.
Model-suggested captures use `source: inference` and should default to reviewable draft state. The
agent may propose 1-3 distilled memories at checkpoints or task end, but humans decide whether to
save them and which scope they belong to. The product goal is remembering the right conclusion at
the right scope, not remembering everything.
Long-form notes are still valid when the user explicitly asks to save a long note, write-up, meeting
summary, research log, or troubleshooting record. In that case OpenMemex behaves like a Markdown
second-brain target: store the note as user-authored content, preserve the body, tag it for retrieval,
and keep the same scope/privacy rules. The distinction is intent: implicit/model-suggested capture
distills; explicit "save this note" may preserve long-form text.
---
## 4. Scopes, Visibility & Namespaces
**Scope = ownership** (who owns it). **Visibility = read access** (who may read it). Defined separately.
| Scope | Owner | Lives where | Synced? |
|------------|-------|-------------|---------|
| `personal` | you | this machine only | **never** |
| `project` | repo collaborators | `<repo>/.ai/open-memex/` | via git (opt-in per repo) |
| `org` | org members | dedicated org memory repo | via git |
Legal combinations: `personal/*` (any visibility, stays local); `project/{internal,shared}`;
`org/{internal,shared}`. `visibility: private` inside a shared scope is a logical boundary
today, not a physical one: `export` excludes private memories by default (D40), and the
`personal` scope never leaves the machine — but a private file placed under
`<repo>/.ai/open-memex/` still travels with git, so review before you push. Physical
isolation of private memories into a local-only cache directory remains planned
(see docs/SCOPES.md); `.gitignore` and pre-commit scanning are defense in depth, not
the boundary.
**Namespace:** `repo_id` (`git-origin-sha256:…`, falling back to a normalized cwd hash) identifies
the same project across machines — carried over from v1's scope-key design.
`team` and `public` scopes are **reserved**: the schema enum rejects writes to them. Rationale for
keeping them reserved: `project` scope already expresses team sharing; a distinct `team` scope needs
a clear semantic difference (e.g. cross-repo team) before it earns existence.
---
## 5. Knowledge Promotion Flow
Explicit promotion only — never automatic sync.
```
personal observation
│ memory propose <id> --to project
▼
proposed ──► reviewed ──► approved ──► published/shared
│ │
rejected (PR review = the mechanism for project scope)
│
│ curator promotes upward later
▼
org repo (cross-project; curated)
```
**Operations-level contract:**
- `open-memex propose` stages the memory file(s) for review. Default: generates the file set and prints
the `gh pr create` command for the human to run (no surprise branches). Solo/no-CI path:
`open-memex propose --local-approve` (records `approved_by` = self, skips PR).
- Core is **not** bound to GitHub PRs — `review_state` transitions are provider-agnostic; GitHub PR
is one review backend.
- The curator role is a documented convention, not a permission system (until Phase 5 signals).
**AGENTS.md distillation** (assisted, human-approved): `open-memex distill` proposes promoting stable,
high-confidence `role: instruction` memories into AGENTS.md. A human always approves. AGENTS.md is the
slow-changing distilled core; memory is the fast-changing long tail. (Per iron rule 2, personal-scope
instructions are never candidates.)
**Maturity narrative** (for README): Observation → Memory → Verified Memory → Shared Knowledge.
---
## 6. Repository Layout
```
<project-repo>/
├── .ai/open-memex/ # default (D23); configurable (alt: .ai/memory/)
│ ├── 01K6AB….md # one memory = one file ("reduces unrelated merge conflicts…")
│ └── …
├── AGENTS.md # constitution + ONE pointer line to the memory system
├── README.md # human-facing; no memories
└── docs/ # human-authored formal docs (ADRs, guides)
```
- The in-repo directory name is **configurable** (`memoryDir` in config): default
`.ai/open-memex/` (D23 — `.ai/` namespace + brand clarity, no collisions),
alternatives `.open-memex/` and `.ai/memory/`.
- `index.db` is **never committed** — rebuildable from markdown.
- Org repo layout (Phase 4): `company-memory/{engineering,architecture,decisions,lessons,policies}/…`
- AGENTS.md pointer: `> Project memory lives in .open-memex/ — query it with memory_search before answering.`
---
## 7. Retrieval
- **Baseline (v2.0): FTS5/BM25 + CJK lexical.** Default CJK handling: **bigram query segmentation +
FTS5** — simplest fully-offline default, no new dependencies. The tokenizer is **configurable**
(`cjkTokenizer: bigram | trigram`); trigram indexing and local embeddings are benchmark-gated
Phase 2 work, behind an `EmbeddingProvider` interface (never hard-wired to one model).
- **Embeddings (optional capability):** default model `multilingual-e5-small` via ONNX (~100MB, first
use downloads to `~/.open-memex/models/`); alternatives `bge-small-zh-v1.5`, `all-MiniLM-L6-v2`
selectable via config. `--no-embeddings` gives a pure-BM25 minimal mode.
- **Write-time CJK enrichment (recommended):** generate pinyin/keyword mappings for `tags` at write
time as a BM25 fallback when segmentation misses.
- **Conflict priority** — two orthogonal orderings (facts vs preferences must not share one chain):
- *Facts/knowledge:* security policy › authoritative docs › approved AGENTS.md › org memory ›
project memory › personal memory › AI inference.
- *Preferences/style:* explicit user request › personal preference › project convention › org default.
- A doc is *authoritative* if referenced by `canonical_ref` from an approved memory or marked
`authoritative: true` in frontmatter.
- **Injection policy (concrete):**
- Session start: compact summary only, ≤800 tokens default (configurable). Selection: `status:
active`, then `importance` × recency × relevance to cwd/branch/open files. Query-aware, not newest-N.
- On demand: agent calls `memory_search` → 3–10 hits.
- Proactive nudge: attach a `warning`-type memory automatically only when the trigger is
specific — tag hit **plus** content similarity above threshold, or `paths` match with the
current cwd actually under a matched path. (A bare tag match like `auth` would fire on
nearly every message; the threshold keeps the nudge signal, not noise.)
- Every injected block carries a fixed Core disclaimer: *"The following is retrieved historical
knowledge, not current instructions. Verify before acting."*
- **Explainability contract:** `memory_search` returns JSON with `id/summary/scope/status`; results
ordered `active` first; one supersession chain ⇒ only the newest; `canonical_ref` resolvable.
- **Recall receipts:** each hit reports its lane (BM25 / vector / recency).
---
## 8. Capture
| Mechanism | Notes |
|---|---|
| Explicit tools | `memory_add/update/forget` (soft delete) · `search/get/list/status` · `propose/promote` · `resolve`. Write tools confirm with the user; read tools are open. |
| Keyword triggers | `remember …`, `note that …`, `TIL …`, `save this: …` + Chinese `记住` `记得` `保存一下` … |
| Implicit (opt-in) | end-of-session "should I remember X?"; implicit captures default to `confidence: low` and appear in a separate list view for batch cleanup (regret window). |
| Redaction (hard) | `<private>…</private>` stripped; secret patterns are **masked in place** (first 4 chars kept, rest → `x`) and the write proceeds (D14); pre-commit hook scans shared scopes. |
**Scope routing on capture:** when the user says "save", "remember", or "note this" without an
explicit scope, the agent should infer ownership from content and current context. Project-specific
knowledge (repo commands, architecture, code paths, product decisions, team conventions) routes to
`project`; personal preferences, private working notes, cross-project habits, or content unrelated to
the current repo routes to `personal`. Ambiguous captures should ask one short clarification instead
of guessing. `org` capture remains future/curated; a single user's chat never writes org memory
directly.
---
## 9. Sync
- **Git is a transport, not the product boundary; the local SQLite index is the query layer.**
Retrieval never walks git. Git/GitHub is the default sharing provider because it gives teams branch,
PR, review, and audit semantics they already trust, but Core must remain provider-agnostic.
- one-memory-one-file ⇒ concurrent edits almost never conflict.
- **Pull is explicit** (`open-memex pull`), never automatic on session start — no surprise context changes.
`pull` = `fetch` + fast-forward only; never auto-commit/push (hard rule for enterprise environments).
- **Merge policy — three conflict classes:**
- *file conflict* (same file, both sides edited): `open-memex resolve` assists YAML-frontmatter merges.
- *semantic conflict* (two memories assert contradictory facts): **Core never silently resolves** —
both stay `active`, flagged for human review.
- *lifecycle conflict* (both sides supersede differently): surfaced, human decides the chain.
- **Offline-first:** every write lands locally first; sync is idempotent reconciliation, not a
transactional push. Airplane-mode writes queue and reconcile on next `push`.
- **Repo identity across rename/fork/migration:** `repo_id` follows the git origin; on drift, the CLI
offers `open-memex migrate` (explicit, never automatic — same rule as v1's scope-key migration).
- **Git unavailable:** projects without git (or with unreachable remotes) remain fully usable.
`personal` scope works everywhere; `project` scope degrades to local-only and `open-memex status`
annotates it as such. Sync commands fail with a clear message, never with a broken state.
- **Portability:** because Markdown is the source of truth, memories should be exportable without
depending on git. A future `open-memex export` command can archive selected scopes/types/tags into a
portable zip/tar bundle (markdown + manifest) for moving to another computer or importing into
another application. Export excludes `visibility: private` memories by default; `--all` / `-a`
includes everything, for a full personal migration to a new machine. API-backed exporters/providers (Notion, Obsidian-compatible vaults, internal
knowledge systems, enterprise stores) are extension paths over the same memory files and admission
rules, not separate products.
---
## 10. Remote Sample — explicitly non-production reference
`examples/remote-server/` — a **minimal, explicitly non-production** reference implementation proving the
`RemoteProvider` interface: Node + SQLite + HTTP, docker-compose, pluggable auth (token for the demo),
`search`/`get`/`pull`/`propose` endpoints — **`propose`, not unreviewed write** — plus an append-only
audit log. No fine-grained ACL, no HA, no backup, no encryption: LAN-only by design.
**Sync semantics:** versioned append + conflict detection; conflicts return `409 Conflict`.
Last-write-wins is **demo-only** and must not appear in any production provider.
Remote candidates are re-admitted through the local admission barrier before indexing.
Truth stays in local markdown; the remote is a sync relay.
---
## 11. Trust & Security
- **Admission barrier — trigger points (explicit):** (a) at write — tool / keyword / import;
(b) before writing any externally-sourced content to local markdown storage (covers sync pull,
remote candidates, and lazy indexing alike — the point is the *write to markdown*, not the
indexing step). "Retrieved memories are never re-ingested" is enforced at
these points, not as a slogan.
- **Security Principle #1:** memory is data by default, never instructions (§3.2 iron rules enforce it).
- **Prompt-injection screening on shared files is heuristic, not a security boundary.** The real
boundary is the data/instruction separation (role gate), not detection.
- **Provenance on every memory:** `source` + `confidence` + `via` + namespaced author identity.
- **Audit log** for all shared-scope writes (who promoted what, when).
- Shared scopes get secret scanning in CI/pre-commit — memory files in a company repo are a new
exfiltration surface; treat them like code.
---
## 12. Relationship to AGENTS.md / README / docs
| Artifact | Answers | Author | Churn |
|---|---|---|---|
| AGENTS.md | "How should AI work here?" — static instructions | human (curated) | low |
| README / docs / ADR | "What is this project?" — formal knowledge | human | low |
| **OpenMemex** | "What did we learn?" — decisions, lessons, preferences | AI-captured, human-reviewed | high |
| org memory | "What does the company know across projects?" | curated | medium |
**Routing rule:** *"Must every agent edit obey it?" → AGENTS.md. Formal long-lived knowledge → docs.
A decision/lesson/preference retrieved per task → memory. Holds across projects → org memory.*
Memory stores summaries and points at docs via `canonical_ref` — never copies them.
---
## 13. Protocol Compatibility & Versioning
- Frontmatter schema is versioned (`schema_version`); JSON Schema published per version.
- MCP tool schemas, CLI contract, and `MemoryProviderCapabilities` are part of the protocol.
- Backward-compat rule: vN readers must read vN-1 files; writers write current version; migrations are
explicit CLI commands, never silent.
## 14. Failure Modes
- Unparseable YAML ⇒ file quarantined (logged, skipped), never crashes indexing.
- SQLite locked ⇒ reads serve stale index + warning; writes queue locally.
- Remote unavailable ⇒ local cache serves with a `freshness` indicator; sync retries explicitly.
- Schema mismatch ⇒ explicit `open-memex migrate`, never silent upgrade.
## 15. Privacy & Data Retention
- `memory_forget` hard-deletes locally; `expires_at` is garbage-collected.
- Session/retrieval/audit logs are retained on a short rotation; full prompts are never logged unless
diagnostics mode is on.
- **Git-history warning:** deleting a file does not purge it from git history. Secrets committed to a
shared memory repo require history rewriting — the pre-commit hook exists to prevent this, not fix it.
## 16. Comparison / Prior Art
| Project | Approach | OpenMemex differs by |
|---|---|---|
| squirrel-memory | `.memory.md` in own repo + MCP | protocol-first; type/role iron rule; promotion flow |
| @ixmachina/memory | private GitHub repo, Markdown+YAML (git = storage + transport) | markdown as source of truth; git is one transport among several; local SQLite is the query layer |
| mcp-memory | single-file Python MCP, ripgrep over git | full lifecycle + scopes + trust model |
| basic-memory | markdown-first, Obsidian-compatible | agent-instruction safety (role gate); org topology |
Differentiators: the instruction/data iron rule, the knowledge promotion flow, and local-first as a
principle rather than a deployment option.
## 17. Adoption Path (solo dev, 5 minutes)
```
npx open-memex init # ~/.open-memex, default config, MCP snippet for your IDE
npx open-memex mcp # start MCP server; prints config for opencode/Cursor/VS Code/Claude Code
```
Zero-config is survival for an open-source project. The opencode plugin remains one adapter among many.
---
## 18. Roadmap
- **Phase 0 — Design freeze + validation (now).** Freeze this doc (protocol v0.2). Verify: company
VS Code MCP-server support; corporate Copilot local-tool support. **Hard gate before Phase 2.**
- **Phase 1 — Local hardening (1–2 wks).** CJK default (bigram+FTS5) · v1→v2 migration · dedup +
lifecycle · redaction hardening · scope docs. No external dependencies.
- **Phase 2A — MCP server (shipped 2026-09-27, D15).** Core/adapters split
(`src/tools/ops.ts`) · MCP server (`src/mcp.ts`, stdio) exposing all eleven memory tools —
read-only-first phasing dropped per D15 · query-aware injection stays host-side.
Ships in **`0.3.0-alpha`** (with bin/npx user-friendliness polish per §17 adoption path).
- **Phase 2B — Team sync.** GitProvider · `propose/promote/resolve` · in-repo dir · 1–2 colleague pilot
(pilot project selection is maintainer-private, not tracked in this doc).
Ships in **`0.4.0`** (was labeled `0.3.0-beta` before 0.3.0 shipped as stable).
Embeddings/rerank run as a **parallel benchmark-gated experiment**, not on the critical path.
- **Phase 2C — Native agent plugins (candidates, not committed).** Claude Code plugin and/or
Codex plugin as hook-enhanced paths over the same MCP tool surface (`SessionStart` →
context injection, `UserPromptSubmit` → keyword-triggered search, `Stop`/`PostToolUse` →
capture); per D16, no host-specific extraction intelligence — opencode is likewise
supported as a plain MCP consumer. Gated on real-world signal from 0.3.0-alpha MCP
dogfooding.
### Agent integration matrix
| Agent | Integration path | Native hooks? | Status |
|---|---|---|---|
| opencode | native plugin (`src/index.ts`) | ✅ keyword capture + first-turn injection | shipped (Phase 1) |
| VS Code Copilot | MCP server + `.github/copilot-instructions.md` | ❌ — VS Code extension API cannot intercept Copilot Chat (researched 2026-09-27); an extension would add no hook capability, so not worth building | ships `0.3.0-alpha` |
| Cursor | MCP server + rules | ❌ no chat plugin API | ships `0.3.0-alpha` |
| Claude Code | MCP server today; plugin + hooks candidate | ✅ `SessionStart` / `UserPromptSubmit` / `PostToolUse` | Phase 2C candidate |
| Codex (CLI/IDE) | MCP server (`[mcp_servers]` in config.toml / `codex mcp add`) today; plugin + hooks + marketplace candidate | ✅ hooks mirror Claude Code's | Phase 2C candidate |
### Competitive landscape (for future positioning)
Coding-agent memory is crowded; open-memex's wedge is **zero-cloud, zero-account,
zero-embedding-download**, with repo-native markdown as source of truth (maintainer
requirement: personal data never touches third-party services). Benchmarks to track:
| Product | Scale / backing (Sep 2026) | Shape | Gap vs open-memex |
|---|---|---|---|
| Mem0 | ~50k+★, $24M Series A (YC) | universal memory SDK/API, vector+graph, cloud-first | cloud dependency; not repo-native for coding agents |
| Letta (ex-MemGPT) | ~24k★, $10M seed | stateful agent platform, memory blocks | agent runtime, not a drop-in coding-agent memory |
| Zep / Graphiti | ~20–30k★, $12M seed | temporal knowledge graph, enterprise | heavy infra; overkill as a coding vault |
| Cognee | ~15–30k★, $7.5M seed | graph ECL pipelines | ingest-oriented, no coding-agent hooks |
| Supermemory | ~15k★, $2.6M seed | consumer second-brain + SaaS API | cloud SaaS |
| atlaso-labs/codex | Codex marketplace | long-term memory plugin for Codex (hooks + MCP + cloud-sync upsell) | **direct comparable** for a future Codex plugin; their cloud upsell vs our local-first |
(Star counts / funding as of Sep 2026 — re-verify before quoting publicly.)
- **Phase 3 — Org layer.** Org memory repo · `examples/remote-server/` ·
curator convention ✅ `docs/CURATOR.md` (2026-09-29, pulled forward) ·
distill-to-AGENTS.md assist ✅ `open-memex distill-agents` (2026-09-29, pulled forward) ·
export/import archive command ✅ `open-memex export` / `import` (2026-09-29, pulled forward, D40).
- **0.5.0 (stable, 2026-09-29).** Init UX pass: `init --global` user-level editor
wiring (D45) · bare-init auto-detect wires all installed editors (D46) · JSONC
configs left untouched with a paste-ready snippet (D47) · `uninstall` reverses
`init` without touching memory data (D48) · empty config files treated as
blank, not corrupt (D49).
- **0.5.1 (stable).** `--help` accuracy: `mcp` help states the server exposes 11
tools (a superset of the opencode plugin's five memory tools); install hints point
at the stable line instead of `@alpha` (F27).
- **0.6.0 (in development).** Close the install→init gap (D50): postinstall
prints the `open-memex init` pointer (never prompts — CI-safe); bare
`open-memex` on a fresh machine offers to run init on a TTY. Close the
init→first-use gap (D51): init ends with a one-line next-step hint
(`open-memex add` + ask the agent to recall it). Agent-prompt clarity pass
(D52): rewrite both agent-facing prompts (MCP handshake + init template) so
agents execute them correctly. Push-not-poll outbox (D53): the checkpoint
mechanism is retired — the server reports the outbox draft count at session
start and appends it to mutating tool results when non-zero. Install→init
reminder fix (F28): one-line stderr nudge on every CLI entry point until
init runs (npm swallows postinstall stdout).
- **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
sharing needs, fine-grained ACL, audit/compliance mandates · optional API-backed exporters/providers
for enterprise knowledge systems.
## 19. Migration v1 → v2
- Scope rename: v1 `user` → v2 `personal` (automatic mapping; `project` unchanged).
- Times: epoch ms → RFC 3339. `priority: N` → `importance: low|normal|high` (1–3 low, 4–7 normal, 8–10 high).
- `type: instruction` (if any v1 memory used it) → `type: <content-kind>` + `role: instruction`.
- Index is discarded and rebuilt from markdown files. `open-memex migrate --dry-run` previews everything.
- Legacy paths: v1 `.my-o-memory/` repo dirs and `~/.my-o-memory/` config are moved to the
v2 locations (in-repo `.ai/open-memex/`; data root per §4/paths — `OPEN_MEMEX_HOME`, else
`%APPDATA%\open-memex` or `$XDG_DATA_HOME/open-memex`) by `open-memex migrate --to-v2`,
with the pre-migration tree kept as a dated backup.
---
## Decisions (append-only)
- **D1** — Markdown as source of truth, index rebuildable. *Rationale: human-editable, git-friendly,
no lock-in; the single decision that makes team sharing trivial.*
- **D2** — Instruction/data separation via `role` gate (iron rule 1). *Rationale: the #1 failure mode
of memory systems is history being mistaken for current rules.*
- **D3** — `personal` scope never syncs. *Rationale: the trust anchor of the whole project.*
- **D4** — MCP is an interface, not the product identity. *Rationale: transports evolve; the protocol
must outlive them.*
- **D5** — Git is transport, local index is query layer. *Rationale: answers the "git can't do
semantic search" objection by separating the two layers.*
- **D6** — Promotion is explicit, never automatic. *Rationale: personal → shared is a governance
decision, not a sync event.*
- **D7** — Embeddings are an optional capability, BM25+CJK is the default. *Rationale: zero-setup
default; no mandatory 100MB download or native dependency.*
- **D8** — Contested choices become **configurable with a popular default**, not hard-coded.
Applies to: in-repo dir name (default `.ai/open-memex/` per D23), CJK tokenizer (default bigram),
embeddings model (default `multilingual-e5-small`). *Rationale: the five-AI review split on all
three; maintainers shouldn't burn decision capital where config suffices.*
- **D9** — The LAN reference server lives at `examples/remote-server/`. *Rationale: maintainer decision
2026-09-26; "lan" is noise for OSS users, "remote-server" names what it proves (RemoteProvider).*
- **D10** — Project renamed to **OpenMemex** (npm `open-memex`, repo `stoneskin/open-memex`).
*Rationale: `my-o-memory` was opencode-specific; the Memory Protocol positioning needs a broader name.
Fresh repo with no company-account commit history (see migration checklist 2026-09-26); old repo
archived with a pointer. Trademark check on "Memex"/"OpenMemex" required before public launch
(flagged in round-4 review).*
- **D13** — Rename cascade (implements D10 in this doc, 2026-09-26): npm package `open-memex`,
CLI `open-memex` (e.g. `open-memex pull`), config dir `~/.open-memex/`, in-repo dir default
`.open-memex/` (**amends D8's** `.my-o-memory/` default). MCP tool name `memory_search` unchanged.
v1→v2 migration moves legacy `.my-o-memory/` dirs and `~/.my-o-memory/` config to the new paths
(with backup, explicit via `open-memex migrate`). *Rationale: one name everywhere; the old default
was set before the rename decision.*
- **D11** — `type` and `role` remain separate. `type` describes what a memory IS; `role`
(`knowledge` | `instruction`, default `knowledge`) describes how it may be USED. `role: instruction`
is an explicit, reviewable exception and may enter instruction context only when iron rule 1
(`role` + `instruction_state: approved` + `trust_level`) passes. *Rationale: content classification
and behavior authorization are separate concerns — the risky action is entering instruction
context, not the content's wording. Unanimous 5/5 in round-4 AI review, 2026-09-26.*
- **D12** — Pull is explicit by default. Session start never pulls, never blocks on network, and
shows a staleness indicator instead. `sync.autoPull` is configurable, default `false`; even with
`autoPull: true`, a failed pull never blocks the session and every pull emits a receipt.
*Rationale: a memory pull can change agent behavior, so it must be a deliberate, visible act —
predictable offline-first beats silent freshness. Unanimous 5/5 in round-4 AI review, 2026-09-26.*
- **D14** — Secret detection masks instead of refusing. A write-path secret hit is **masked in
place** (first 4 characters kept, the rest replaced with `x`, length-preserving) and the write
proceeds with a notice; `<private>…</private>` spans are still stripped to `[REDACTED]`.
Amends the v0.2 rule "secret patterns refuse the write" (§8, §11). *Rationale: a refused write
loses the surrounding context the user asked to remember; a prefix-masked secret stays
recognizable (which key it was) while the credential itself is not recoverable from the file.
Supersedes the refusal behavior; applies to every write path (tools, keyword capture, CLI).
2026-09-27.*
- **D15** — Phase 2A MCP server ships with all five tools, not read-only first. The MCP server
(`src/mcp.ts`, stdio) exposes `memory_add` / `memory_search` / `memory_list` /
`memory_supersede` / `memory_forget` — amends the §18 roadmap's "Read-only MCP" phasing.
*Rationale: the write path is the same Core (redact/D14, dedup, lifecycle) already shipped and
dogfooded in the opencode plugin, so a separate read-only stage adds process cost without
reducing risk. Core/adapters split implemented as `src/tools/ops.ts` (host-agnostic logic +
shared zod schemas); the opencode plugin and the MCP server are thin adapters over it.
Query-aware injection stays host-side: MCP is request/response and offers no hooks, so
proactive memory use depends on the host's agent instructions. 2026-09-27.*
- **D16** — No separate LLM extraction pass; memory intelligence lives in model-driven tool
calls. A dedicated post-session extraction (opencode `session.idle` hook → hidden session
→ host model) was evaluated and rejected: the model's own decision to call `memory_add`
*is* the LLM judgment of "worth remembering", so a second pass is redundant and
host-specific. Investment goes into the shared layer instead — `TOOL_DESCRIPTIONS` in
`src/tools/ops.ts` and each host's agent instructions — so every host benefits at once.
Native plugins (opencode now; Claude Code / Codex as Phase 2C candidates) remain as
hook-enhanced paths, but opencode is also supported as a plain MCP consumer of
`open-memex mcp`, keeping one unified tool surface. 2026-09-27.
- **D17** — `open-memex init` resolves the MCP server command at init time. A durable
`open-memex` on PATH (outside npm's ephemeral `_npx` cache) → `command: "open-memex"`;
otherwise (one-shot `npx open-memex@alpha init`) → `command: "npx", args: ["-y",
"open-memex@alpha", "mcp"]` plus a hint to `npm i -g` + re-run `init --force`.
`mcp --print-config` uses the same resolution. *Rationale: a one-shot npx run leaves
no bin behind, so writing `command: "open-memex"` would produce a dead MCP server on
the next editor launch; the npx fallback keeps the one-command setup actually
one-command. 2026-09-27.*
- **D18** — Keyword scope routing: 我 → personal, 我们 → project. Chinese capture
keywords are split into two pattern lists: personal patterns (`记住我`/`替我记`/
`我觉得`/`我喜欢`, plus legacy `remember for me`/`记住(个人)`) route to the personal
scope, while project patterns (`我们认为`/`我们决定`/`帮我们记住`, and the generic
`记住…` for `记住我们的…`) route to the current project scope. Personal patterns are
scanned first and *claim* the line so the generic `记住…` pattern cannot double-fire;
`记住我` uses a `(?!们)` guard so it never swallows `记住我们…`. *Rationale: the
user's own rule — "我" is personal, "我们" is the current project — stated 2026-09-27;
scanning user messages (never assistant output) with personal-first claim keeps one
utterance to one memory. README + repo AGENTS.md keyword sections updated in the same
commit. 2026-09-27.*
- **D19** — `open-memex init` asks setup questions; `open-memex config set` edits settings
after install. `init` prompts on a TTY (editor: vscode/cursor/opencode; keyword
auto-capture on/off; first-turn injection on/off), `--yes` accepts all defaults, and
non-terminal runs never prompt (scripts keep the historical vscode default).
Non-default answers persist to the JSONC config file; `open-memex config set <key>
<value>` changes them later (validated keys: `maxProjectMemories`, `maxProfileItems`,
`injectOnFirstTurn`, `keywordCaptureEnabled`, `logLevel`). `init --client opencode`
merges a `type: "local"` MCP entry into project-level `opencode.jsonc` (v1 format).
*Rationale: install time is the only moment the user's attention is guaranteed, and a
print-only `config` left no path to change settings afterwards. 2026-09-27.*
- **D20** — `init` / `mcp --print-config` support Visual Studio. Writes solution-level
`.mcp.json` with the `"servers"` section (`{ "type": "stdio", "command", "args" }`),
per Microsoft Learn (VS 2022 17.14+ / VS 2026, Windows-only). `.github/copilot-
instructions.md` is still written — VS's Copilot reads it too. Note VS also
auto-discovers `.vscode/mcp.json` and `.cursor/mcp.json`, so repos already set up for
VS Code get VS support for free; the explicit `.mcp.json` is the source-controllable
option. 2026-09-27.*
- **D21** — The published npm package ships pre-compiled JS (`tsc -p tsconfig.build.json`
→ `dist/`, via `prepublishOnly`; bin points at `dist/cli.js`). *Rationale: Node's
`--experimental-strip-types` refuses files under `node_modules`
(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so the old launcher
(`bin/open-memex.js` re-execing `src/cli.ts`) crashed on every global install —
reported 2026-09-27 on Node v22.12.0. Development stays build-free (`npm run cli`
/ `npm run mcp` run `src/` directly); `doctor`'s MCP self-check resolves its server
entry the same way it is running (`dist/mcp.js` vs `src/mcp.ts`). The opencode
native plugin still loads `src/index.ts` (Bun strips types anywhere). 2026-09-27.*
- **D22** — `init` writes the Copilot memory instructions to the **user level** by
default (`~/.copilot/copilot-instructions.md`; `%USERPROFILE%\copilot-
instructions.md` for Visual Studio 2026) instead of the repo-level
`.github/copilot-instructions.md`. *Rationale: the repo-level file is checked in,
so teammates without open-memex get Copilot errors about missing `memory_*`
tools. The user-level location is GitHub's official personal-instructions slot
(highest priority, all projects, never in a repo). `--instructions project`
keeps the old repo-level behavior for teams where everyone uses open-memex.
The instructions carry a guard clause ("ignore this section when the
`open-memex` MCP server is not available") as cheap insurance. 2026-09-27.*
- **D23** — Default in-repo memory dir is `.ai/open-memex/` (configurable via
`memoryDir`; rejects absolute paths and `..`). *Rationale: `.ai/` is the
emerging convention for AI-local project state; the `open-memex/` leaf keeps
brand clarity and avoids collisions with other tools' `.ai/` content.
2026-09-27.*
- **D24** — Project-scope markdown lives in the repo: writes with
`scope: project` go to `<projectRoot>/<memoryDir>/` (D23 default
`.ai/open-memex/`); `personal` never leaves appdata. Legacy appdata project
files are lazily migrated on first write/`syncScope` — the move is guarded by
the appdata dir name (which *is* the scope key), so it can only ever migrate
the current scope's files. The index's `file_path` is the single locator for
reads/deletes (`findMemoryFile`, `forget`); on the near-impossible id
collision between locations, the in-repo copy wins. *Rationale: one home per
scope, no silent data loss, no repo pollution before first use.* 2026-09-27.*
- **D25** — Review workflow semantics (`propose` / `promote` / `resolve`,
`src/review.ts`): (1) `review_state` frontmatter field
(`draft → proposed → approved/rejected → published`, default `draft`) plus
`proposed_by` / `approved_by` / `derived_from` / `review_note` provenance;
the SQLite index carries `review_state` (schema v6, rebuilt from markdown —
D1). (2) `propose` **copies** personal → project (new id, never moves — the
personal original stays private); one call takes several ids (one branch,
one PR; all-or-nothing — a bad id aborts the whole batch);
`--local-approve` skips the PR for solo
devs. (3) `promote` advances exactly one step up the ladder
(`proposed → approved → published`), `--reject`s with a note (also from
`approved`, withdrawing approval before merge), or `--resubmit`s a rejected
memory back to `proposed` for another round; `draft` and terminal states
refuse. (4) `resolve` lists conflicted memory files, or
attempts a field-level 3-way merge from git stages 1/2/3 (`tags` union,
`updated_at` takes latest, body merged only when one side changed); semantic
conflicts (same field / body changed differently on both sides) are reported
and the file is left untouched — **never auto-resolved**. (5) A rejection
never deletes anything: the file stays on the author's branch; the human
accepts (close PR, delete branch), revises + `--resubmit`s, or keeps it as
a `[rejected]` record. (6) No git
automation anywhere in the workflow: no branch creation, no commits, no PRs —
the commands print the exact next steps for the human. Org-level promotion is
Phase 4. *Rationale: the review ladder must be explicit and auditable; the
tool assists merging but a human always decides meaning.* 2026-09-27.*
- **D26** — **Supersedes D24.** Project-scope markdown has two homes, one per
lifecycle stage — never silently moved between them. (1) `memory_add` /
`memory_propose` with `scope: project` write to the **appdata outbox**
(`memories/<project_key>/<id>.md`, `review_state: draft`); the outbox is
git-invisible and branch-independent. The D24 lazy migration is deleted —
appdata project files are legitimate drafts, not legacy. (2) `open-memex
submit <id...>` / `memory_submit` moves user-named drafts into the repo's
`<memoryDir>/` (D23), flipping `review_state` to `proposed`: the file now
follows branches and PRs. `sync-status` / `memory_status` shows both sides
(outbox drafts, repo review states, uncommitted repo files). (3) The move is
one-stage-one-place: copy → verify hash → local commit → verify → delete
outbox original; re-running is idempotent (identical content skips, different
content aborts). `search` prefers the repo copy when both exist; conflicts
(same id, different content; push rejected) stop and ask the human — never
overwrite. *Rationale: an AI agent drafts constantly; the repo should only
ever see what the human explicitly approved for review. The outbox is the
agent's desk, the repo dir is the shared table. Approved 2026-09-28.*
- **D27** — **Revises D25 §6 (no git automation).** `submit` automates the
*local* half of the git workflow: create `mem/sync-<timestamp>` (or
`--onto` the current branch for code+memory PRs), copy, `git add` only the
memory files, local commit, verify. It prints the push + `gh pr create`
commands for the human — but an agent that already holds the user's Yes for
this sync carries through push and PR creation without re-asking (each step
is not a separate approval). Batch submit is all-or-nothing; an empty-branch
abort rolls the branch back. *Rationale: the old "no git automation" rule
assumed a human at the keyboard; the agent-driven flow needs local mechanics
automated while push/PR stay under the user's explicit per-sync Yes/No.
Approved 2026-09-28.*
- **D28** — Memory PR base defaults to the **current branch**; the user may
redirect to `main` or the project's integration branch (`--base`). A
standalone memory PR and a code+memory PR are both supported — the agent asks
which one each time; on "with code" it uses `--onto` and never commits
unrelated staged changes. *Rationale: memory usually reviews against the work
it describes (current branch); the integration branch is the exception, not
the default. Approved 2026-09-28.*
- **D29** — Every review transition is written to a per-memory audit trail
(`review_history`: at/by/from/to/note) — reject / resubmit / approve /
publish all append, never overwrite; `propose` and `submit` seed it; merge
conflict resolution unions both sides' histories. *Rationale: review is a
decision log, not a flag — "who rejected this and why" must be answerable
months later. Approved 2026-09-28.*
- **D30** — Retrieval ranks by review state: approved/published project
memories outrank unreviewed content; project drafts/rejected sink to the
bottom and are visibly tagged `[draft]` in search/list/inject output, while
personal memories (always draft by design) are never demoted or tagged.
Explicit search still finds drafts — they are deprioritized, not hidden.
*Rationale: reviewed knowledge should win the context window; drafts stay
discoverable but never masquerade as vetted. Approved 2026-09-28.*
- **D31** — Every index sync records when it ran, what triggered it
(`session` / `request` / `cli` / `submit`) and its stats, in
`<appdata>/sync-state.json`; `sync-status` shows the last sync first.
*Rationale: "is my index fresh?" must be answerable without guessing —
especially across branch switches where the in-repo dir changes underneath.
Approved 2026-09-28.*
- **D32** — The branch PR's GitHub state maps back onto each in-repo
memory's review_state via `pr-status` / `memory_pr_status`: merged PR →
published, PR approval → approved with `approved_by` = reviewer login,
changes-requested → suggestion only (never auto-rejects). Report by
default; `--apply` performs the mapped transitions locally (no push —
inside the D27 line). Each memory keeps its own state: a human `rejected`
is never overridden by a PR signal. *Rationale: the PR is where the team
actually reviews — the mapping closes the loop without inventing new
review UI. Approved 2026-09-28.*
- **D34** — The MCP server sends session-start guidance in the handshake
`instructions`: call `memory_status` at session start (and at work
checkpoints); if the outbox has drafts, summarize and ask the user which to
sync; proactive `memory_add`; `memory_search` before asking about the past;
personal never leaves the machine. *Rationale: the init-written instruction
files only exist if the user ran `init --client` — the handshake reaches
every MCP client at connect time. Still advisory: no MCP consumer offers a
hard session-start hook, and we do not claim otherwise. Approved 2026-09-28.*
- **D35** — "sync memory" (or "同步记忆") is a natural-language trigger for the
sync flow: the agent calls `memory_status`, summarizes the outbox drafts, and
asks the user which ones to sync — same flow as the session-start proposal,
but user-initiated. Taught in the MCP handshake instructions, the
init-written instruction files, and the `memory_status` tool description.
*Rationale: the user should not have to remember command names to sync;
saying it in words must work. Approved 2026-09-28.*
- **D36** — `submit` never creates a branch on its own (revises D26/D27): it
copies the named drafts into `.ai/open-memex/`, commits locally on the
CURRENT branch, and prints next-step commands. Branch creation is the human's
call — or the agent's, only with explicit approval for the full chain, via
`submit --branch <name>` / `memory_submit(branch=...)`. *Rationale: an
auto-created branch strands the user on it — they forget to switch back.
After a submit the agent asks ONE follow-up ("want me to create a branch +
push + open the PR, or will you handle it yourself?") instead of branching
silently. Checkpoints that trigger `memory_status`: session start, end of a
work chunk, after the user commits, and after any memory_* action.
The "sync memory" trigger ALWAYS goes through the `memory_status` tool —
never by browsing the appdata directory directly. Approved 2026-09-28.*
- **D33** — Every CLI command answers `open-memex <command> --help` (and `-h`)
with its own usage, flags, and examples; checked before config/DB load so
help works even in a broken environment. Unknown commands with `--help`
fall back to the global usage. *Rationale: AI assistants discover the CLI
through --help first — a command that silently swallows --help as a flag
teaches the agent nothing. Approved 2026-09-28.*
- **D37** — Conversation distillation is the memory unit; transcript storage is
not the default. Capture should preserve the useful conclusion from a human/AI
session — what was learned, what resolved the issue, what should be avoided,
and where the authoritative doc lives — as a short reviewable memory. If the
knowledge already exists in docs, store a `reference` memory with
`canonical_ref` instead of duplicating the doc. Tags are retrieval hints, not
content kinds: `type` classifies the memory, tags describe topics/tools/paths.
*Rationale: full chat logs are noisy, harder to review, and riskier for
privacy; the value is remembering the right conclusion at the right scope,
with enough context for the next human or AI session. Approved 2026-09-28.*
- **D38** — OpenMemex supports a Markdown second-brain use case while keeping
distillation as the default for implicit/model-suggested capture. If the user
explicitly asks to save a long note, meeting summary, troubleshooting record,
or write-up, preserve it as user-authored Markdown with tags and normal
scope/privacy rules. Git/GitHub sync is the default team-sharing provider, not
the product boundary: future export/import archives and API-backed providers
may move the same markdown memories to other computers, applications, or
enterprise systems. For generic "save/remember/note this" requests, the agent
routes project-specific knowledge to `project`, personal or repo-unrelated
knowledge to `personal`, and asks one clarification if ambiguous; `org` remains
curated/future, never a direct single-user chat write. *Rationale: users also
want a local AI-assisted second brain, but portability and ownership must stay
explicit; sharing mechanisms should be provider choices over the same memory
model, not the identity of the product. Approved 2026-09-28.*
- **D39** — `type` and `tags` are complementary axes, not alternatives. `type` is
single-valued: what the memory IS — it drives behavior (lifecycle, review,
rendering, retrieval: a `todo` can be completed, a `reference` resolves
`canonical_ref`, a `decision` participates in supersede chains). `tags` are
multi-valued: what the memory is ABOUT — pure retrieval hints
(topics, tools, subsystems, paths, incidents). *Rationale: without type the
system cannot tell "a todo I must do" from "a fact I must know" even when both
are tagged `auth`; without tags, cross-cutting retrieval ("everything about
onboarding") would need a combinatorial type explosion. Type answers "how do I
handle this?", tags answer "how do I find this?". Approved 2026-09-28.*
- **D40** — `open-memex export` excludes `visibility: private` memories by
default; `--all` / `-a` includes everything, for a full personal migration to
a new machine. *Rationale: the safe default protects privacy; the escape hatch
keeps the "no walled garden" promise — the user can always take everything
with them. Approved 2026-09-28.*
- **D42** — §3.5 checkpoint distillation is taught in the MCP handshake
instructions (`src/mcp.ts` `SERVER_INSTRUCTIONS`) and the `init`-written
instruction files (`src/init.ts` `INSTRUCTIONS`): at checkpoints the agent
DISTILLs the session and proposes 1–3 short memories (conclusion, not
transcript); nothing is saved without user approval. Approved captures are
saved via `memory_add` with the new optional `source` param set to
`"inference"` (default `"tool"`). *Rationale: closes the 0.4.0 TODO from D41 —
the design's capture loop now reaches the agent. Implemented 2026-09-29.*
- **D43** — `distill-agents` output gains a "Memory hygiene (open-memex)"
footer carrying the §3.5 checkpoint guidance (propose 1–3 distilled captures
at checkpoints; save nothing without approval; prefer `reference` over
copying). *Rationale: double insurance for opencode users, who never see the
MCP handshake instructions or the `init`-written instruction files — but
opencode reads AGENTS.md natively, so the distilled snippet teaches the
checkpoint habit wherever it lands. Approved 2026-09-29.*
- **D44** — `migrate --to-v2` hotfix (issue #7, 0.4.1): (1) `--dry-run` now
previews the legacy `my-o-memory` files in place — per-file conversion plans
without moving anything (previously it scanned only the new, still-empty
root and always reported "0 files", making the preview useless); (2) dry-run
no longer prints the false "legacy data dir merged" line; (3) the Windows
EPERM on the legacy-dir backup rename is caught and rethrown as an actionable
message (close the program holding the folder — e.g. a running MCP server —
and re-run; nothing was deleted), and the CLI prints it as `Error: …` with
exit 1 instead of a raw syscall stack; (4) `parseFlags` accepts `--key=value`
in addition to `--key value` (the `=` form was silently misparsed before,
dropping the flag — which can turn a `--dry-run` into a real run).
*Rationale: a preview that shows nothing is worse than no preview — the user
cannot confirm what the real run will do; and a destructive-path failure must
speak in user terms. Shipped as 0.4.1 hotfix on the stable line. Approved
2026-09-29.*
- **D45** — `init --global` (0.5.0-alpha.1): one-time user-level MCP wiring for
VS Code / Cursor. Writes the open-memex server entry to the editor's
user-level `mcp.json` (`%APPDATA%\Code\User\mcp.json` on Windows,
`~/Library/Application Support/Code/User/mcp.json` on macOS,
`~/.config/Code/User/mcp.json` on Linux; `~/.cursor/mcp.json` for Cursor)
instead of the project's `.vscode/mcp.json` — init once, the server starts
in every project. The entry keeps `cwd: "${workspaceFolder}"` so VS Code
substitutes it per window and project-scope resolution keeps working;
a per-project config still wins when present. Merge semantics are shared
with project-level init (merge, never clobber; `--force` overwrites).
Interactive `init` now asks per-project vs user-level for vscode/cursor
(default: per-project, preserving old behavior); `--global` skips the
question. For opencode, `--global` is a no-op that prints the native-plugin
one-liner (already global, and strictly more capable than plain-MCP mode);
Visual Studio stays solution-level by design. *Rationale: per-project init
is a paper cut that compounds — the data layer already needs zero per-project
setup (scope is derived from cwd), so the editor wiring should be able to
match. Approved 2026-09-29.*
- **D41** — The type taxonomy is reconciled to 11 types with one-line definitions
(§3.1): `fact` `preference` `decision` `constraint` `todo` `knowledge` `howto`
`gotcha` `lesson` `observation` `reference`. Merged away: `warning`→`gotcha`,
`workflow`→`howto`, `incident`→`lesson` (incident id goes in `tags`),
`architecture`→`knowledge` (tag `architecture`). *Rationale: a type earns its
place only if the system treats it differently (lifecycle, retrieval,
rendering); otherwise it is a tag. Fifteen types blur classification and hurt
agent accuracy — eleven keeps each type's behavioral slot distinct. `lesson`
is deliberately broader than `incident`: a postmortem's shape (timeline, root
cause, actions) is a template concern, not a type. Code
`MEMORY_TYPE_TAXONOMY` updated to match. Approved 2026-09-28.*
- **D46** — `init` with no `--client` auto-detects installed editors and wires
them all (0.5.0-alpha.2). Detection: VS Code via `code` on `PATH`, well-known
install locations, or an existing user-level `mcp.json`; Cursor via `cursor`
on `PATH` or `~/.cursor`; opencode via `opencode` on `PATH` or its global
config dir; Visual Studio only when the project has a `.sln` (solution-scoped
by design). Auto mode always wires user-level where the editor supports it —
VS Code / Cursor MCP entry (D45), and for opencode the native plugin entry is
now *actually merged* into `~/.config/opencode/opencode.json` (replacing D45's
hint-only `--global`), so one init covers every editor and every project.
A config file with comments (JSONC) is never rewritten — init prints the
manual one-liner instead. Interactive `init` shows the detected editors and
confirms wiring all of them (declining falls back to the single-editor
prompt); non-interactive (`--yes`) wires all detected with no prompts.
An explicit `--client` keeps the old single-editor behavior, including the
per-project default for vscode/cursor. *Rationale: Stone's two hats — he
writes code in several editors himself, and new users / pilot colleagues
should not have to learn `--client` to get started. The help text already
promised "default: auto-detect"; D46 makes the code keep that promise.
Approved 2026-09-29.*
- **D47** — when `init` refuses to touch a config file that isn't valid JSON
(usually JSONC with comments — VS Code / Cursor / opencode all accept them),
it now prints the exact snippet to add by hand: the section key plus the
`"open-memex"` entry, pretty-printed. Covers all three MCP-writing paths —
vscode/cursor (user + project level), opencode project-level `opencode.jsonc`,
and Visual Studio `.mcp.json` — matching the manual hint the opencode
`--global` plugin path already printed (D46). The file is still never
rewritten; the hint just makes "fix it manually" actionable. Triggered by
Stone's real Windows run: his user-level `mcp.json` had comments, init
correctly left it alone, but the old message gave him nothing to paste.
*Rationale: a refusal without a remedy is a dead end; the entry shape is
already computed, so printing it costs nothing. Approved 2026-09-29.*
- **D48** — new `open-memex uninstall` command, the reverse of `init`
(0.5.0-alpha.4). Removes the MCP server entry / opencode native plugin line /
Copilot instructions section that `init` wrote; memory data is never touched.
Client resolution mirrors `init` (explicit `--client`, else auto-detect with
an interactive confirm). Without `--global` it cleans both project-level and
user-level files — init may have written either, and a leftover entry at the
other level would be a surprise; `--global` restricts to user-level. Empty
sections/arrays are pruned; an instructions file that only held the
open-memex section is deleted. Non-JSON (JSONC) configs are left untouched
with the manual step, same D47 treatment as the init write path.
*Rationale: Stone asked "有 uninstall 吗" while testing init on Windows —
every write deserves an undo. Approved 2026-09-29.*
- **D49** — empty config files are no longer treated as corrupt (0.5.0-alpha.5).
Stone's real Windows run: his user-level `mcp.json` existed but was empty,
and `init` refused it with "not valid JSON — left untouched", because
`JSON.parse("")` throws. New `parseJsonConfig` helper: empty or
whitespace-only content parses as `{}` — there is nothing to preserve, so
writers safely populate it; genuinely unparseable content (JSONC comments)
or non-objects still return null and keep the D47 leave-untouched + manual
hint behavior. Applied to all five config file touch points: the three init
writers (vscode/cursor MCP, opencode `opencode.jsonc`, Visual Studio
`.mcp.json`) and the two uninstall removers (empty file = nothing to remove).
*Rationale: an empty file is the safest write target, not a corrupt file;
refusing it sent the user down a manual path for no reason. Triggered by
Stone's report 2026-09-29.*
- **D50** — close the install→init gap (0.6.0-alpha.1). `npm install -g`
only puts the CLI on PATH; the editor wiring is `init`'s job, and a clean
reinstall wipes it — Stone hit exactly this on 2026-09-30 (fresh opencode
reinstall + `npm i -g open-memex`, then no open-memex in `opencode.jsonc`).
Two changes, both CI-safe: (1) a `postinstall` script that **prints**
`Run \`open-memex init\`…` — postinstall must never prompt, it runs in CI /
Docker / `npm ci` where stdin isn't a terminal; (2) bare `open-memex` on a
machine where init never completed **offers** to run it (default yes) when
stdin+stdout are TTYs, otherwise prints usage exactly as before. Asked-state
is a `.init.json` marker at the data root — init writes it on success, a
declined offer writes it too, so the question is asked once; `uninstall`
removes it (unwiring is the reverse of init, so the next bare run offers to
wire again). The marker is a dotfile and export builds from DB rows, so it
can't leak into bundles. The offer re-execs `open-memex init` as a child
with inherited stdio rather than calling init in-process — the offer's own
readline already consumed stdin's buffer, and a second readline on the same
stream would see EOF on burst input (verified with a pty test).
*Rationale: install ≠ setup, and the gap only shows up on a fresh machine —
exactly when the user has the least context. A printed hint covers the
install moment; the interactive offer covers the first-run moment; neither
can hang a pipeline. Approved 2026-09-30.*
- **D51** — close the init→first-use gap (0.6.0-alpha.2). D50 gets the user to
a wired editor; a first-time user then stops at "it's wired" with no idea
what to do next. `init` now ends with one concrete next step:
``Next step: `open-memex add "standup is at 9:30"` — then ask your agent what
it remembers.`` One line, printed unconditionally — the cheapest possible
onboarding after wiring. *Rationale: the CLI is editor-independent, so the
hint works no matter which client was wired; `add` + recall is the smallest
loop that proves the whole system works. Approved 2026-09-30.*
- **D52** — agent-prompt clarity rewrite (0.6.0-alpha.3). Both agent-facing
prompts (MCP `initialize` instructions in `src/mcp.ts`, init instruction
template in `src/init.ts`) are rewritten to fix ambiguities found on review:
(1) the proactive-save vs approval-gate contradiction is resolved by naming
the two cases — facts the user *states* are saved proactively, conclusions
the agent *infers* are proposed first and saved only on approval;
(2) `memory_status`/`memory_search` are excluded from the "after any
memory_* action" checkpoint trigger (self-trigger loop);
(3) tool names use the full `memory_*` form in both prompts;
(4) the four checkpoints are defined once as "Checkpoints" and referenced,
with an operational heuristic ("a task the user would describe in one
sentence") replacing "meaningful chunk of work";
(5) empty outbox → do nothing; "the user commits" → "any git commit in this
session"; `type "reference"` named explicitly; PR-base mechanics spelled out
per D28. *Rationale: these prompts are the product's UI for agents — a
literal-minded agent must execute them correctly without guessing.
Approved 2026-10-01.*
- **D53** — push-not-poll outbox: the checkpoint mechanism is retired; code
pushes state to the agent instead (0.6.0-alpha.4). `src/submit.ts` gains
`outboxDraftCount()` (one indexed SQLite COUNT on the current scope's outbox,
no git I/O); `src/tools/ops.ts` gains `withOutboxNote()`, which appends
`[open-memex: N draft(s) waiting in the project outbox — call memory_status
to review]` to mutating tool results, only when N > 0. It is wired into the
five MCP mutating tools (`memory_add`, `memory_supersede`, `memory_forget`,
`memory_submit`, `memory_propose`) and the three opencode-plugin mutating
tools (`memory_add`, `memory_supersede`, `memory_forget` — there the note
stays generic because the plugin has no `memory_status`). The MCP server
also appends the live draft count to the `initialize` instructions when N >
0 (stdio servers start fresh per session, so construction-time state is
session-start state). Both agent-facing prompts drop the Checkpoints section
and the post-`memory_submit`/`memory_propose` status checks; the sync rule
becomes one line ("when the server reports drafts waiting, call
`memory_status`") plus the existing "sync memory" trigger. The static init
template keeps one explicit session-start `memory_status` call, since a
static file cannot carry live state. An anti-nag clause is added: if the
agent already asked about these drafts this session, it does not ask again.
*Rationale: the server knows the outbox state; making the agent poll for it
on a timer wastes tool calls and teaches a habit that scales badly. The note
is silent when the outbox is empty, so the common case costs nothing.
`memory_submit` drains the outbox, so its own note is naturally silent.
Approved 2026-10-01.*
- **F28** — install→init reminder was invisible (0.6.0-alpha.5). The D50
postinstall pointer never reaches the user: npm runs lifecycle scripts in
the background and swallows their stdout unless `--foreground-scripts` is
passed (reproduced on npm 10.9.4 — the script ran with code 0, its output
never displayed). The install-time channel is therefore best-effort only.
Fix: every CLI entry point now prints a one-line nudge on stderr until init
has run or been declined (`open-memex init` wires editors), gated by the
existing `.init.json` first-run marker — stderr keeps the MCP stdio protocol
(stdout) intact, so even `open-memex mcp` spawned by an editor carries it
safely. `init`/`uninstall` are excluded. The bare-`open-memex` interactive
offer on a TTY (D50) is unchanged. *Rationale: the reminder must live in a
channel the project controls — the CLI — not in npm script output.
Reported 2026-10-01.*
- **F29** — Copilot review fixes on PR #11 (0.6.0-alpha.7). (a) The lockfile
carried a stray `"version": "0.6.0-alpha.1"` key as a direct child of
`packages` (left by the D50 version bump; hand-edited bumps preserved it) —
`npm ls --package-lock-only` failed on it. Removed; version bumps now go
through `npm pkg set` so npm owns the lockfile format. (b) D53 missed the
shared `TOOL_DESCRIPTIONS.memory_status`: it still told agents to call the
tool "at session start and at task checkpoints" — the polling this change
retires. Now: session start, server-reported drafts, or explicit "sync
memory". Same staleness removed from the `source` field hint. (c) Onboarding
strings (postinstall note, first-run nudge, bare-CLI offer) now mention
Visual Studio auto-detection for solution projects instead of listing only
three editors. *Lesson: never hand-edit version fields in package-lock.json.
Reported 2026-10-01.*
- **D54** — Agent Skills support (0.6.0-alpha.8). Ship a bundled
`open-memex` skill (`skills/open-memex/SKILL.md`: frontmatter + CLI guide —
proactive save, search, scope routing, outbox→submit flow) inside the npm
package. `init` copies it (not symlinks — Windows needs no Developer Mode)
into each wired editor's user-level skills dir: VS Code →
`~/.copilot/skills/open-memex/`, Cursor → `~/.cursor/skills/open-memex/`,
opencode → `~/.config/opencode/skills/open-memex/`; Visual Studio has no
skills concept and is skipped. User-level by design (D46: init once). An
existing skill is never clobbered silently — kept unless `--force`.
`uninstall` removes only the `open-memex` skill directory. The skill tells
agents to prefer MCP tools (`memory_add` etc.) when available and fall back
to the CLI otherwise (with the `npx -y open-memex@latest` prefix when the
CLI isn't on PATH). *Rationale: skill-aware agents get memory with zero MCP
configuration; the skill is the CLI-shaped complement to the MCP server.
Requested by Stone 2026-10-01 after the Agent Skills ecosystem suggestion.*
- **F30** — Sweep pre-rename `my-o-memory` leftovers (0.6.0-alpha.9). The
my-o-memory → open-memex rename left editor configs behind that still load
the OLD plugin/server, which writes to the OLD data dir — silently splitting
the user's memories in two (found live on Stone's work machine 2026-10-01:
the old opencode plugin sat above the new one in `opencode.json`, so
opencode and the CLI wrote to different data roots). `init` now drops stale
`my-o-memory` plugin entries and `my-o-memory` MCP server keys wherever it
touches a config (opencode global plugin list, per-project `opencode.jsonc`,
`.mcp.json`, VS Code/Cursor `mcp.json` incl. `--global`), even on the
"kept" path; JSONC files it can't parse get a manual-removal hint.
`uninstall` sweeps the same leftovers. `doctor` gains a read-only
`legacy my-o-memory` check: fails when a legacy data dir exists next to the
data root, when a user-level config still references `my-o-memory`, or when
`MY_O_MEMORY_HOME` points at a pre-rename dir. *Lesson: a package rename
must clean up its own old wiring — the old entry and the new entry are never
both valid. Reported 2026-10-01.*
- **D55** — opencode's native plugin supersedes the Agent Skill
(0.6.0-alpha.10). Real-world feedback (Stone 2026-10-01): with both the
native plugin's `memory_*` tools and the D54 skill installed, opencode's
agent turned chatty — two overlapping instruction sets plus a "use MCP or
CLI?" choice on every memory action. No data corruption (identical saves
dedupe), but the UX regressed from "one tool call and done" to narration.
`init` no longer installs the skill for opencode when the native plugin is
wired (`--global`), and removes a previously installed one; the per-project
plain-MCP mode keeps the skill as the no-tools fallback. VS Code/Cursor
keep the skill — their MCP wiring is more fragile (server-start failures,
policy blocks), so the fallback still earns its keep, and the skill already
tells agents to prefer MCP tools when available. *Lesson: don't ship two
overlapping integrations for the same editor — the agent pays the
duplication cost in chatter. Reported 2026-10-01.*
- **D56** — question-shaped searches and host parity, said out loud
(0.6.1-alpha.1). Two-persona review (Stone 2026-10-03) converged on small,
honest fixes rather than new machinery. (1) Retrieval already OR-ed terms
and ranked with bm25, but fed whole questions ("how do we configure…",
"中文记忆怎么检索") straight into FTS — question words matched everything
and diluted ranking. Query construction now filters a small EN/CJK
function-word set, dedupes, and caps terms at 24 per side, with a
never-empty fallback (query.ts; partially answers the retrieval-robustness
open question — layers 2 agent-side expansion / 3 capture-time aliasing
remain open, embeddings stay reserved). (2) The plugin(5)/MCP(11) tool
asymmetry is now stated in the six MCP-only tool descriptions themselves,
with the CLI equivalent named, so an agent looking for `memory_status`
inside opencode finds the answer instead of assuming breakage (the
host-parity open question's "document it" option; true parity undecided).
(3) `init`'s closing lines gain a one-line privacy statement at the moment
of maximum doubt: memories live only on this machine, personal ones never
leave it, nothing is uploaded.
- **D57** — explainable search, a memory audit, and the concurrency posture
written down (0.6.1-alpha.2; addresses most of the retrieval-explainability
/ memory-health open question and establishes the concurrent-sessions
behavior it asked for). (1) `open-memex search --explain` prints the
constructed FTS expression, per-hit bm25 score, and how many matches
lifecycle hid (superseded vs retracted/archived), so a miss is diagnosable
— bad query, stale memory, or chain-hidden — without reading the index.
(2) New `open-memex audit` (read-only; doctor checks the environment, this
checks the memories): near-duplicate active pairs (same ≥ 0.8 Jaccard as
write-time dedup), actives untouched for 90+ days, broken supersede chains,
personal files found inside the in-repo memory dir (iron-rule breach), and
index/file drift. Reports only; fixes stay with `supersede` / `forget` by
hand. (3) Concurrency, measured: every process opens its own connection to
one WAL index; memory files are per-memory ULIDs, so concurrent writes to
*different* memories never collide on disk. SQLite was the gap — no
`busy_timeout` was set, so better-sqlite3 silently tolerated 5s while
bun:sqlite (opencode plugin) timed out at 0 and failed instantly under any
overlapping write. Now unified at `PRAGMA busy_timeout = 5000` in `db()`.
Past the timeout the CLI prints one plain line ("store is busy … nothing
was lost") instead of a stack trace — and nothing *is* lost: the memory
file is written before the index update, so the next sync reconciles it
(verified end-to-end: a write blocked 8s still lands once the lock frees).
Memory file writes are tmp+rename, so a concurrent reader never sees a
torn file. Same-memory concurrent edits remain last-writer-wins (accepted:
different processes editing the same memory simultaneously is a human
conflict, not a storage one). `doctor` reports the live locking settings.
- **D58** — the opencode plugin must never runtime-import the host SDK
(0.6.1-alpha.3). Field report: after a global `npm i -g open-memex`, the
plugin was configured in `~/.config/opencode/opencode.jsonc` but silently
inactive — no memory tools, no error, nothing in opencode's log. Bisected
with the real host (opencode 1.18.34) and a minimal plugin: a bare
`file://` entry loads fine; adding `import { tool } from
"@opencode-ai/plugin/tool"` makes opencode skip the plugin without a
trace. That import only ever resolved from a source checkout (where
`npm install` provides the devDependency); from a global install's
location nothing provides the module and the host does not resolve it
for file:// plugins. The helper is a runtime identity (`return input`),
so the fix is a local same-signature stand-in in `tools/memory.ts` —
types still come from the SDK (type-only imports erase at runtime), and
the plugin now depends only on real runtime dependencies. Verified in
the real host against a global-install-shaped tree: all five tools
register. Rule going forward: plugin code imports host SDK types only;
runtime imports must resolve from the package's own dependencies.
- **D59** — `doctor` patrols the plugin entry it used to take on faith
(0.6.2-alpha.1). D58's failure was invisible to every existing check:
`doctor` verified the MCP server end-to-end but never looked at the
opencode plugin entry, so a configured-but-dead plugin passed all
checks. The new `opencode plugin` check reads the user-level opencode
configs (`opencode.json` + `opencode.jsonc`), attributes entries to
this plugin by package path or the `[open-memex]` marker (source
checkouts live anywhere), and fails when (a) the entry's target file
does not exist — with the `init --force` re-point command named — or
(b) the entry's relative-import closure runtime-imports
`@opencode-ai/plugin`, the exact D58 death. It checks only what can be
attributed; per-project and MCP wiring are out of scope.
- **D60** — one plugin serves both opencode generations (0.7.0-alpha.1).
Opencode 2 changes the plugin contract end to end: config reads
`plugins` (directories or `{package}` objects) instead of v1's
`plugin` file entries, a v1-style file entry only earns a log warning
("configured plugin path must be a directory") and is silently skipped
— the D58 death in a new shape; and the plugin value itself is a
structurally decoded `{ id, setup(ctx) }` rather than the v1 server
factory. A single `open-memex init --client opencode --global` now
writes both keys (`plugin` file URL and `plugins` directory): each
host loads from its own key — verified: opencode 1.18.34 ignores the
`plugins` array with a config-normalization warning, and opencode 2
warns per `plugin` file entry but loads the directory entry — so users on either generation get a
working plugin from the same install. `src/index.ts` default-exports
`{ id, setup, server }`: v2 calls `setup`, v1 calls `server`, the
shared logic (bootstrap, keyword capture, one-time injection) lives in
`src/plugin-core.ts`. Cost of the bridge on the v1 side: v1 hosts
older than 1.18.29 only invoke function default exports and would
skip this object; 1.18.29+ accepts it — the README states the new
minimum. The v2 adapter (src/opencode-v2.ts) obeys D58's rule — no
runtime `@opencode/plugin` import (a devDependency again); the
context is duck-typed from the 2.0 surface, tool arg shapes compile
from the same zod shapes via `z.toJSONSchema`, and hooks map
one-to-one (session `prompt` → keyword capture, session `context` →
first-turn injection). One registration subtlety cost real debugging
time and is worth recording: on 2.0.1 a plugin tool added without
options is exposed to the model only in code mode — callable through
an `execute` meta-tool that runs model-written JS
(`tools.memory_search(...)`) — and a model that instead calls the
tool by name gets a host-side "Unknown tool" error, so whether the
memory tools work at all depends on the model's calling style. Each
tool therefore registers with `options: { codemode: false }`, which
restores plain function calling (verified: direct calls execute
first try). The D59 doctor check extends to both keys:
v2 entries are checked for an existing, resolvable entry file and the
closure scan now covers `@opencode/plugin` too, plus a generation
advisory — when the installed host and the config point at different
generations, doctor fails and names the fix (`init --force`; package-
name entries installed via `opencode plugin add` are recorded but not
path-checked). Field-verified on real hosts 1.18.34 and 2.0.1.
- **D61 — proactive capture cluster: propose posture, a shared
memorability rubric, and capture-time aliasing (retrieval layer 3).**
Builds on the "proactive capture" open question below and the
CHECKPOINT_PROACTIVITY / MEMORY_VISIBILITY notes. Three pieces,
shipped together because they are one user-visible behavior — the
agent capturing well without being asked each time:
(1) **Posture: propose, not auto-draft.** When the agent finishes a
task the user would describe in one sentence, it proposes 1–3 short
candidate memories in the conversation and saves only what the user
approves (approved inferences are saved with `source: "inference"`,
the §3.5 convention). The guidance is text, not mechanism — the same
rubric written into the bundled skill, the AGENTS.md memory-hygiene
footer, the MCP handshake instructions, the `memory_add` tool
description, and a one-line summary in the injected context block —
with throttling rules (task end / decision / gotcha moments only, no
mid-task interruptions, never re-propose a declined candidate). The
auto-draft posture (write first, surface later in bulk) is deferred
until memory visibility ships: without an inventory the user can
read, silent drafting reads as surveillance, so D61 deliberately
does not build it.
(2) **Rubric.** One memorability standard, shared by this live
capture path and the future retrospective-distillation group:
worth saving — decisions and their reasons, preferences,
conventions, gotchas, approaches tried and abandoned, and
`reference` memories pointing at authoritative docs; not worth
saving — one-off task details and anything re-derivable from the
code. Retrieval robustness layer 2 (agent-side query expansion:
decompose into concepts, 2–3 phrasings each, cross-language, check
the other scope) ships as guidance in the same texts, on top of the
D56 server-side layer 1.
(3) **Capture-time aliases (layer 3).** When a memory is saved, the
agent attaches up to 4 aliases — alternate phrasings, synonyms,
equivalents in the user's other language — stored in a new optional
frontmatter field `aliases` (additive; files without it parse
unchanged, `schema_version` stays 2) and indexed in their own FTS
column (index schema 6→7, rebuilt from markdown automatically).
Gated by a new `captureAliases` config flag (init asks once, default
on, `config set captureAliases off` opts out): when off, aliases
passed by agents are dropped and the alias guidance disappears from
the MCP handshake. Turning it off never rewrites existing memory
files — aliases already stored stay stored and searchable, and
supersede keeps carrying them forward; the flag gates new captures
only (no silent data destruction). Aliases pass through the same
secret redaction as memory content before they are stored, and a
malformed alias list (blanks, more than 4) is normalized silently —
never a tool-call error.
Keyword capture (no agent in the loop) leaves
aliases empty rather than inventing them; `supersede` carries
aliases forward unless the caller replaces them. Aliases are
normalized (trim, case-insensitive dedupe, cap 4) and bad ones cost
nothing — they rank as bm25 noise, never an error. Retrospective
distillation over host transcripts stays its own group; it will
reuse this rubric.
- **D62 — environment variable names grow current-name aliases.** The
data-root and config-path overrides predate the rename and kept the
old package's name (`MY_O_MEMORY_HOME`, `MY_O_MEMORY_CONFIG`) — a
stale-looking name is a trap for users and agents scripting against
the current one. `OPEN_MEMEX_HOME` / `OPEN_MEMEX_CONFIG` are now the
preferred names; the legacy names still work as fallbacks (new name
wins when both are set), so existing setups and scripts never break.
The data root itself does not move — only the variable names change.
`doctor`'s storage check notes when the root arrived via the legacy
variable, and its pre-rename-dir leftovers check reads whichever
variable is in effect.
- **D63 — correctness fixes from a whole-project external review.** Six
verified silent-wrong-answer bugs, fixed as one batch: (1) VS init
dropped the parsed `.mcp.json` instead of merging it, wiping other
servers; (2) forget reported success when unlink failed — only ENOENT
is success now, both in the tool op and the CLI; (3) the config
loader's regex comment-stripper was not string-aware and corrupted
redact patterns (`src/**/secrets` → `srcsecrets`) — replaced with a
string-aware scanner now shared with doctor (one parser, not two);
(4) `loadConfig` enforces the `memoryDir` no-escape invariant on
hand-edited files, falling back to the default with a warning;
(5) `migrate` rewrites `scope`/`visibility` with the key, and
conflict resolution compares parsed timestamps (lexicographic
RFC-3339 order breaks at fractional seconds); (6) bundle import
enforces staging-dir containment (resolve + realpath), rejecting
`../` and symlink escapes. Rule going forward: any behavior that can
fail must fail loudly in its return value; "already gone" is the only
swallowed delete error.
- **D64 — second batch from the same review: stale answers, stranded
files, and holes between surfaces.** (1) The opencode plugin hosts
synced the index only at bootstrap, so memories written by the CLI or
another client stayed invisible all session (and exact duplicates
accumulated); plugin tool calls now reconcile from disk first,
mirroring the MCP server's per-call sync. (2) Superseding a published
memory reset `review_state` to `draft` in the repo, where submit
could not reach it and promote refuses drafts — the replacement now
re-enters at `proposed` whenever the old memory had entered review.
(3) Every memory-file rewrite now goes through the atomic
tmp+rename helper (D57 covered creates only); a torn read mid-rewrite
previously parsed as a deletion. (4) `setStatus` refuses
`retracted → active`: retraction is one-way through this path, or
withdrawn content silently returns to recall. (5) Propose copies get
`visibility: internal` instead of inheriting `private`, which export
excludes by default — the team copy no longer vanishes from bundles.
(6) `opencodeGlobalConfigPath` resolves whichever of `.json`/`.jsonc`
actually exists, as init's writer does; uninstall no longer reports
"nothing to remove" while leaving the plugin entry behind.
(7) `doctor` now checks all eleven MCP tool names (it checked the
plugin's five) and flags v1 memories stranded under `memories/user/`,
which v2 read paths cannot see. (8) `memory_status` output is capped
per section with an "… and N more" line; `memory_list` accepts
`scope: both`; `forget` says when the deleted file lived in the repo
(commit the deletion); the Copilot instructions and the MCP handshake
now tell the agent the same thing about when to call
`memory_status`; the AGENTS.md distillation footer teaches
`source: "inference"` like every other surface; `where` prints the
config path it always promised. Standing rule reinforced: any surface
that answers a question must be reading current truth, and every
workflow state must have a next step.
- **D65 — keyword capture: natural phrasings fell through.** Default-pattern
fixes found while verifying a review claim: (1) `帮我记住…` — arguably the
most natural Chinese phrasing — matched no pattern at all; it now captures
to personal, following the established 我-rule (same as 替我记), while
`帮我们记住…` still routes project. (2) A leading `请` defeated every
记住-led pattern (`请记住我…`, `请记住:…`, `请记住(个人)…`); the
`(?:请)?` prefix the 记一下 patterns already had is now uniform.
(3) English `remember to …` left a stray leading "to" in the captured
body; the pattern consumes it (with backtracking verified so
"remember Toronto…" is not mangled). Patterns are code defaults, so
upgrades pick these up automatically unless the user customized the
pattern lists. Deliberately NOT changed: multi-line paste triggering
and the secret-masking windows stay as they are — narrowing them is a
capture-recall tradeoff that needs an explicit decision, not a drive-by.
- **D66 — follow-up review of the D63–D65 stack.** (1) The 帮/替
keyword family is rebuilt around full verb phrases: an optional `请`,
`记` + optional `住|录` + optional `一下`, then a separator boundary —
`请帮我记住:…` captures (D65's new pattern lacked its own `请`),
`帮我记录一下:…` saves `周五不发布` instead of the mid-word garbage
`录一下:…`, and `帮我记得…` does not fire. `help me remember: …`
joins the personal patterns. Still deliberately unchanged: shortening
the ≥3-char content floor (it eats `记一下这个`-type captures, but
lowering it invites junk) and adding narrative triggers like `记得…` /
`remind me to…` (imperative-vs-narration ambiguity) — both need an
explicit decision. (2) `sync-status` CLI shows the full list; the
20-line cap binds only agent renders, whose "… and N more" line names
the CLI. `list --scope both` reaches CLI parity with the tool, and
`status --help` documents the retracted one-way door. (3) The test
suite is Windows-safe (probe imports are `file://` URLs, the uninstall
test sets `USERPROFILE` since `os.homedir()` ignores `HOME` there)
and refuses to run against a stale `dist/`. (4) Sync-error posture is
now a stated asymmetry: the MCP adapter lets pre-call sync errors
propagate as tool errors; the plugin adapter logs them at debug and
answers from a possibly-stale index rather than taking the tools
down. (5) §19's legacy-paths text now names the shipped locations;
historical decision entries keep their as-decided wording.
- **D67 — keyword capture: a trigger must be a statement to the store.**
Three rulings, all from the D66 review's open items plus one bug D66
introduced. (1) **The boundary guard guards the bare verb only.** D66 put
a separator requirement *after* the whole verb phrase, which killed the
commonest Chinese form — `帮我记一下这个配置`, `帮我记住这个配置`,
`替我记一下我住在杭州`, `帮我们记一下这个约定` all stopped matching, because
the noun follows 记一下/记住 directly. The guard now applies only to a
bare 记: `记` + (`住`|`录`, never followed by a verb continuation like
得/着) + optional `一下`, or a separator. So mid-word garbage (`帮我记得带伞`
→ `带伞`) is still impossible while the natural form captures. (2) **The
≥3-char body floor stays, but stops being silent.** A trigger that yields
`这个` or `OK` is a match on the trigger, not a memory, and a 2-char memory
is noise in every later injection — so the floor is deliberately *not*
lowered (a script-aware floor is the escape hatch if short Latin bodies
ever matter). What changed is honesty: `capture --dry-run` prints the
rejection with the reason, and `logLevel: debug` logs it, so "nothing
happened" is never a mystery. A rejected **personal** match also keeps
its line claimed — otherwise `记住我:OK` fell through to the generic
记住 and was saved as the nonsense project memory `我:OK`. (3) **Narrative
forms are not triggers.** `记得…`, `remind me to…`, `I always forget to…`
stay out: they are imperatives aimed at the agent, they are the most
frequent sentences in any conversation that mentions memory, and a false
trigger writes permanent state nobody reviewed — while a missed trigger
costs one sentence. The asymmetry is deliberate: precision over recall.
For the two ambiguous forms already in the list, an explicit marker now
separates narration from instruction: `别忘了:…` / `don't forget: …` /
`don't forget that …` capture; bare `别忘了带伞` / `don't forget the wifi
password` do not. The `note` family is untouched — `note the API is v2`
was never ambiguous. (4) Test/tooling honesty: `test-full.ts` guards the
built `dist/` with a content fingerprint (`scripts/build-stamp.mjs`)
rather than mtimes — `git checkout` rewrites mtimes, so a branch switch
could leave an older-branch dist looking fresh — and the agent-facing
`memory_status` cap is pinned again (20 entries per section plus a
"full list: open-memex sync-status" pointer) after D66's CLI-parity test
had dropped that coverage.
- **D68 — memory visibility: the inventory is a protocol surface.** Group 3-C
(design: `docs/memory-visibility-design.md` v2). `memory_list` output
becomes a numbered, structured inventory (`[type] id=… created=…
source=…`, state tags for project scopes; `created=` is when the fact
was learned, `updated=` shown only when it moved materially later). The
raw enums stay in the data; plain-language renderings live in the
guidance surfaces (tool descriptions, MCP handshake, SKILL.md,
distill-agents footer) so agents rephrase in the conversation's
language. New input `include: "active" | "all"` (default `active`) —
a deliberate behavior change: superseded versions and retracted /
archived memories leave the default listing (audit view via `all`).
Every listing states its true total; truncation is disclosed, never
silent (the D66 lesson applied to this surface). `open-memex
inventory` renders the same data layer as text (default) or JSON
(`open-memex-inventory/1`); a report containing personal memories
refuses to be written inside a git working tree without
`--allow-personal`. `memory_forget` gains `soft` (CLI `forget
--soft`): retraction as a first-class tool action — hidden from lists
and search, file kept, still one-way per D64 (restoring the fact means
saving it again; no un-hide, no undo, no recycle bin). Deletion stays
a confirmed conversation: agents re-list before acting on a number
(numbers are only valid for the listing just produced) and read the
entry back before deleting. The HTML rendering of the same data layer
follows as a separate change; auto-draft stays gated on the unlock
conditions in the design doc.
- **D74 — the Node floor is 22.14, because the driver sets it, not us.** D72
moved the Node driver to `better-sqlite3` 13 (the N-API rewrite) to stop the
Node 24.19+ exit-path abort. That upgrade carried an unstated requirement:
v13 is compiled against **Node-API 10**, and Node only gained Node-API 10 in
**22.14.0**. Below that floor `require("better-sqlite3")` succeeds and then
`new Database()` segfaults the process — no message, no stack, exit 139 on
macOS/Linux, `0xC0000005` on Windows. Measured on win32-x64: Node 22.12.0
dies on the first open, Node 22.23.3 and 24.20.0 work; Node 20 can never work
with v13, since Node-API 10 never landed on that line (EoL regardless). This
is upstream and still open — WiseLibs/better-sqlite3#1514, plus #1516 for the
separate `npm ci` gyp problem on Windows, where `gypfile: false` does not stop
npm from shelling out to node-gyp. `engines.node` had been `>=22.6`, which was
the `--experimental-strip-types` floor — a real but *different* constraint
that predated the driver bump; leaving it would have shipped a package whose
declared support window crashes on its own dependency. Three changes:
(1) `engines.node` is `>=22.14.0`, and the two floors are documented as two
floors rather than merged into one number; (2) `loadDatabase` refuses below
the new floor and throws the actionable message (upgrade Node, or pin
`better-sqlite3@^12.11.1` — v12 is unaffected), because a crash no in-process
`try`/`catch` can report is the worst possible failure mode for a CLI;
(3) `doctor` reports the floor *and* probes the driver in a **child process**,
since a segfault cannot be caught in-process — the probe resolves the driver
from this package's own directory, because `doctor` routinely runs with cwd
set to a user project where a bare `require` finds nothing. The version
comparison lives in `src/runtime-floor.ts`, pure and unit-tested, because the
boundary is a fact to be tested (22.13.9 below, 22.14.0 exactly at, 22.23.3
and 24.20.0 above) rather than a constant to be trusted. Deliberately NOT
done: pinning back to v12 (that re-opens D72's Node 24 abort), or attempting
to fix either upstream bug here.
- **D73 — one user statement, one memory: the keyword hook hands off to the
agent.** The keyword hook and the agent are two writers for one user turn,
and only one of them knows the sentence is already stored. Observed
2026-10-04: a single `remember this folder is for my spark tank ideas …
shared memory across agents and employees` produced **three** memories — one
`source: keyword` verbatim capture plus two `source: tool` paraphrases the
agent wrote because it could see the chat transcript but not the store. The
existing §3.4 guard could not catch it: token Jaccard scored those
paraphrases 0.21–0.38 against the capture, below the 0.8 near-dup bar, and
`open-memex audit` reported zero near-dup pairs, so the store looked healthy
while holding three copies of one statement (retrieval then returns all
three). Two mechanisms, deliberately layered:
**(a) hand-off note** — `captureFromText` records each capture against the
host session id (`chat.message` / v2 `prompt`, both of which carry one), and
the system-context hook (`experimental.chat.system.transform` / v2
`context`) pushes a short block naming what was already stored, delivered
once per capture and pruned after the turn window. The agent can now avoid
the duplicate write it had no way to see. In-memory only, like
`injectedSessions`: never persisted, gone on restart.
**(b) turn-echo guard** — `memory_add` additionally refuses a write that
overlaps a `source: keyword`/`user` memory created in the same scope within
the last 10 minutes, returning the stored id and pointing at
`memory_supersede`. This is the deterministic backstop for the case where
(a) is ignored. It is deliberately scoped and high-barred, because lexical
overlap **cannot** separate an agent's gloss from a genuinely new fact: in
the same measured set, the agent's gloss scored 0.211 and a distinct fact
the same sentence happened to contain scored 0.214. So the bar sits at
0.28 — wholesale re-saves of the captured sentence measure 0.29–0.36 and are
refused; anything below is left to (a) and the agent's judgment, and a
refusal always names the stored id so the caller can rephrase to the new
part or supersede. Agent-authored `source: tool` saves keep the old 0.8
near-dup *notice* and are never refused. The refused case is recoverable by
construction (supersede, or a follow-up add carrying only the new
information), and no D14-style masking, dedup, or lifecycle rule changes.
What is deliberately NOT done: no fuzzy-matching upgrade (D73 measures why a
better metric still cannot decide "new information"), no auto-merging of the
agent's gloss into the capture, and no transcript store (the replay-distillation
open question owns that).
- **D72 — the Node SQLite driver moves to the N-API line.** The D70 suite was stable on Windows and failed in shifting clusters on Linux under Node 24.20: individual CLI processes intermittently died with `SIGABRT` in `Statement::~Statement()` (`RemoveEnvironmentCleanupHook ... Assertion failed: (env) != nullptr`) and lost their stdout, so unrelated supersede / submit / inventory checks failed depending on which process the GC reaped. The work itself had always landed; only the exit path was broken. Root cause is upstream: Node 24.19 changed `node::ObjectWrap` cleanup-hook registration, and `better-sqlite3` <13 (raw `node::ObjectWrap`) can finalize a prepared statement from a GC callback with no Environment entered. `better-sqlite3` 13 is the N-API rewrite and removes that path; it still supports Node ≥22, and every API this repo uses (`Database`, `prepare`, `exec`, `pragma`-style PRAGMAs, `close`) is unchanged. The suite's crash-retry shim stays as belt-and-braces for older installs, but the shipped driver no longer needs it on current Node. Verified: three consecutive Linux `test-full` runs at 114/114 plus a 40-command add/list/search loop with zero aborts (was ~50% per process).
- **D71 — the scope seed is the folder opened, never a filesystem root.**
OpenCode v1 hands the plugin two roots: `worktree` (the repo root — but
the filesystem root itself, `C:\` or `/`, when the opened folder is not a
git repo) and `directory` (the folder actually opened). The v1 adapter
preferred `worktree`, so every non-git folder on a drive seeded its scope
on the drive root and collapsed into one shared
`project__workspace__<hash of the root>` bucket — a `c:\temp` project
landing in `project__workspace__3dc8472eaebe` (seed `c:\`) is what
surfaced it. `pickScopeRoot` (src/scope.ts) now takes `worktree` only when
it is a real directory below the filesystem root, else `directory`. The
repo-root behavior is unchanged: a real worktree still wins, so opening a
repo subfolder keeps the repo's scope. Memories already written under a
root-seeded key stay where they are (no auto-migration, per the existing
rule); `open-memex scopes` shows the stray bucket and
`open-memex migrate --from <old-key>` moves them. The v2 adapter already
seeded on the opened directory and is unaffected.
- **D70 — visibility follow-ups: one number, one guard, one truth.** The
3-C review of D68/D69 found the surfaces honest individually and
inconsistent with each other. (1) **Every visibility surface discloses
truncation.** `memory_list` did; `open-memex list` — the command a
sceptical user runs first — silently showed 20 of 25. It now renders
the same structured line as the tool and states how many entries it
dropped, naming the command that shows everything. (2) **Numbers are
unique within a listing.** Ordinals ran per scope, so a single
`scope: both` output contained two `#1`s and "delete #3" was a guess;
the counter now runs across every section of one listing (tool, CLI and
the text report). (3) **A correction that touches the repo says so.**
The hard delete already did (D64); the soft hide did not, so a
retraction of an in-repo memory read as "hidden" while it was only a
local working-tree edit — the team still had the memory. Both surfaces
now name the commit and push. (4) **The report guard fails closed and
exists once.** `isInsideWorkTree` ran `git -C` on the output's parent
directory, so a not-yet-created directory made the check throw and
report "not in a worktree" — a fail-open on the one path where personal
memories would ride a commit; it now resolves the nearest existing
ancestor, reports a missing directory as itself, and both formats share
a single refusal helper instead of two copies. (5) **`open-memex-inventory/2`**
drops the absolute `file` path: the JSON renderer exists to be handed to
an agent, i.e. pasted into a cloud conversation, and it carried the
user's home directory. (6) Smaller honesty fixes: an empty scope is
reported as `(0)` instead of vanishing; only the HTML audit view loads
the hidden rows themselves (text/json pay for counts); the page's
"no network requests" claim is now enforced by a `default-src 'none'`
CSP rather than asserted; and `countList`'s comment states its one known
divergence (a self-healed dangling chain pointer) instead of claiming
equivalence. Deliberately NOT done, pending a decision: HTML i18n and a
size cap for very large stores.
## Open Questions
_Some early questions are resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull). The entries below are open unless marked decided._
- **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. **Update (2026-10-03): decided in D61 — propose posture** (agent proposes 1–3 candidates at task checkpoints, saves only on approval; auto-draft deferred until memory visibility ships, see the MEMORY_VISIBILITY question below).
- **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. **Update (2026-10-03): layers (2) and (3) shipped in D61** — agent-side expansion as guidance text on every surface, capture-time aliases indexed in their own FTS column behind the `captureAliases` flag. Embeddings stay deferred.
- **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.
- **Non-programmer users: PMs, BAs, and entry points (open, 2026-10-03):** the current experience assumes a programmer's toolchain — terminal, npm install, editor plugin, git branches, PR review. Project managers and BAs have none of that, and their most likely entry points are not the VS Code chat window: at most an agentic CLI (opencode, Claude Code), more probably Teams chat, MS Copilot, or another chat surface calling the same MCP server. The mechanics must hide behind two confirmations ("remember this?" / "share with the team?") — scope/promote/supersede/outbox vocabulary is a programmer dialect they should never have to learn. Three roles to design for, in increasing order of build cost: (1) **sources** — their meetings and decisions are the highest-value project memory, captured by someone else's agent with attribution in `source`; needs nothing new. (2) **curators** — "is this true? is it safe to share?" is business-truth review, which BAs are often better placed to do than programmers; GitHub's web PR review is the only existing no-CLI door, and CURATOR.md would need a plain-language path for reviewers who have never touched git. (3) **direct users** — capture sources shift from chat commands to meeting transcripts, email, and tickets; and without git there is no courier: a shared-folder transport is mechanically easy (memories are Markdown files) but has no PR-equivalent review step, so a substitute review flow has to be designed. Also weigh: stakeholder/customer facts carry PII and retention weight that code conventions never do, so "is it safe to share" becomes a compliance question, not a hygiene one. If a company ever gives every employee one unified agent entry point (a work buddy / AI team mate), most of this audience split dissolves — the question is what open-memex should assume until then: heterogeneous entry points, one memory service behind them.
- **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.
- **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.
- **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.
- **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.
- **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. **Status (2026-10-03): decided (D68)** — the inventory ships as structured list + text/JSON report with conversational rendering taught to agents; "one-step forget/undo from the view" was deliberately not built (deletion stays a confirmed conversation; soft-hide added instead); the HTML rendering follows separately. Auto-draft remains gated on the unlock conditions in `docs/memory-visibility-design.md`.
---
*End of draft v2.*