open-memex 0.7.2 → 0.7.3-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ .git
package/CHANGELOG.md CHANGED
@@ -9,6 +9,22 @@ the `@alpha` tag are development snapshots and are not listed individually.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ - Tool-definition quality pass (Glama TDQS 4.0/5 on v0.7.2): fixed `memory_pr_status` declaring `readOnlyHint` while `apply=true` mutates local files; added explicit MCP annotations (read-only / destructive / open-world) to all 11 tools; descriptions now route between sibling tools (propose vs submit vs promote, resolve vs pr_status, forget vs supersede), state error behavior and real defaults (search limit 8, list limit 20), and the search/list `type` and `limit` parameters are documented. D76 records the rule; `scripts/smoke-mcp.ts` guards the pr_status annotation.
13
+
14
+ - Design doc maintenance (D75): pruned resolved Open Questions (decided in D56/D57/D61/D68, plus the shipped retrieval-robustness layers) and recorded four competitor-scan directions — retrieval evaluation gate, per-client trust levels, cold-start import, and a `doctor` mis-registration check.
15
+
16
+ - Dropped the deprecated `user` value from the MCP tools' advertised `scope` enums (`personal` is the only global-scope name now; the storage layer still migrates v1 `user` data automatically).
17
+
18
+ - Added a root `Dockerfile` (multi-stage, starts the stdio MCP server via `node dist/cli.js mcp`) so container-based directories such as Glama can build, start, and introspect the server.
19
+
20
+ - Both READMEs now open the Core concepts section with an architecture diagram (English and Chinese versions in `docs/assets/`).
21
+
22
+ ## [0.7.2] - 2026-10-04
23
+
24
+ ## Listed in the official MCP Registry
25
+
26
+ - **Added: open-memex is now published in the official MCP Registry** as `io.github.stoneskin/open-memex`, so MCP clients and directories can discover and install it from the registry. The npm package declares the matching `mcpName` in `package.json` (the registry verifies package ownership against it), and the repo carries `server.json` with the registry metadata; its version fields move in step with package releases.
27
+ - Release runbook: commands are now given one per step with explicit stop-and-check gates, including "wait for npm to propagate before mcp-publisher publish".
12
28
  - Added `docs/release-runbook.md`: the maintainer release checklist (version sync points, preflight, npm alpha/stable publish, MCP Registry publish, Node 22.14 floor notes).
13
29
 
14
30
  ## [0.7.1] - 2026-10-04
package/Dockerfile ADDED
@@ -0,0 +1,18 @@
1
+ # Container for MCP introspection (Glama) and containerized runs.
2
+ # Starts the stdio MCP server: `open-memex mcp`.
3
+ FROM node:22-slim AS build
4
+ WORKDIR /app
5
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \
6
+ && rm -rf /var/lib/apt/lists/*
7
+ COPY package*.json ./
8
+ RUN npm ci
9
+ COPY . .
10
+ RUN npm run build
11
+
12
+ FROM node:22-slim
13
+ WORKDIR /app
14
+ COPY --from=build /app/dist ./dist
15
+ COPY --from=build /app/node_modules ./node_modules
16
+ COPY package.json ./
17
+ ENV NODE_ENV=production
18
+ CMD ["node", "dist/cli.js", "mcp"]
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/open-memex.svg)](https://www.npmjs.com/package/open-memex)
6
6
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
7
+ [![open-memex MCP server – quality and maintenance score on Glama](https://glama.ai/mcp/servers/stoneskin/open-memex/badges/score.svg)](https://glama.ai/mcp/servers/stoneskin/open-memex)
7
8
 
8
9
  [中文文档](./README.zh-CN.md)
9
10
 
@@ -86,6 +87,8 @@ open-memex doctor
86
87
 
87
88
  Three ideas explain almost everything open-memex does.
88
89
 
90
+ ![open-memex architecture: your editors share one local memory — Markdown files as the source of truth, an SQLite FTS5 index for search, personal scope that never leaves the machine, and project scope shared through git PRs](docs/assets/open-memex-architecture-en.png)
91
+
89
92
  **1. Two scopes: `project` and `personal`.**
90
93
  Every memory belongs to one of two places:
91
94
 
package/README.zh-CN.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/open-memex.svg)](https://www.npmjs.com/package/open-memex)
6
6
  [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
7
+ [![open-memex MCP server – quality and maintenance score on Glama](https://glama.ai/mcp/servers/stoneskin/open-memex/badges/score.svg)](https://glama.ai/mcp/servers/stoneskin/open-memex)
7
8
 
8
9
  [English README](./README.md)
9
10
 
@@ -71,6 +72,8 @@ open-memex doctor
71
72
 
72
73
  三个概念能解释 open-memex 几乎所有的事。
73
74
 
75
+ ![open-memex 结构示意:你用的编辑器共享同一份本地记忆——Markdown 文件是事实来源,SQLite FTS5 索引负责检索,个人记忆只留本机,项目记忆经 Git PR 分享](docs/assets/open-memex-architecture-zh.png)
76
+
74
77
  **1. 两种作用域:`project` 和 `personal`。**
75
78
  每条记忆属于两个地方之一:
76
79
 
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- f8c82ea850a33c28a396d33675b9d22b1b99cf19cef5e828252fb0400b1154e6
1
+ c5a82fa9f711dea3b1e8787d551424e978d606435b08f83d51ba5aaf0996e9ff
package/dist/mcp.js CHANGED
@@ -164,51 +164,57 @@ export async function runMcpServer() {
164
164
  server.registerTool("memory_add", {
165
165
  description: TOOL_DESCRIPTIONS.memory_add,
166
166
  inputSchema: z.object(memoryAddArgs),
167
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
167
168
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))));
168
169
  server.registerTool("memory_search", {
169
170
  description: TOOL_DESCRIPTIONS.memory_search,
170
171
  inputSchema: z.object(memorySearchArgs),
171
- annotations: { readOnlyHint: true },
172
+ annotations: { readOnlyHint: true, openWorldHint: false },
172
173
  }, withSync((args) => toMcp(searchMemories(getScope, args))));
173
174
  server.registerTool("memory_list", {
174
175
  description: TOOL_DESCRIPTIONS.memory_list,
175
176
  inputSchema: z.object(memoryListArgs),
176
- annotations: { readOnlyHint: true },
177
+ annotations: { readOnlyHint: true, openWorldHint: false },
177
178
  }, withSync((args) => toMcp(listMemories(getScope, args))));
178
179
  server.registerTool("memory_supersede", {
179
180
  description: TOOL_DESCRIPTIONS.memory_supersede,
180
181
  inputSchema: z.object(memorySupersedeArgs),
182
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
181
183
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))));
182
184
  server.registerTool("memory_forget", {
183
185
  description: TOOL_DESCRIPTIONS.memory_forget,
184
186
  inputSchema: z.object(memoryForgetArgs),
185
- annotations: { destructiveHint: true },
187
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
186
188
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))));
187
189
  server.registerTool("memory_status", {
188
190
  description: TOOL_DESCRIPTIONS.memory_status,
189
191
  inputSchema: z.object(memoryStatusArgs),
190
- annotations: { readOnlyHint: true },
192
+ annotations: { readOnlyHint: true, openWorldHint: false },
191
193
  }, withSync((_args) => toMcp(statusMemories())));
192
194
  server.registerTool("memory_submit", {
193
195
  description: TOOL_DESCRIPTIONS.memory_submit,
194
196
  inputSchema: z.object(memorySubmitArgs),
197
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
195
198
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))));
196
199
  server.registerTool("memory_propose", {
197
200
  description: TOOL_DESCRIPTIONS.memory_propose,
198
201
  inputSchema: z.object(memoryProposeArgs),
202
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
199
203
  }, withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))));
200
204
  server.registerTool("memory_promote", {
201
205
  description: TOOL_DESCRIPTIONS.memory_promote,
202
206
  inputSchema: z.object(memoryPromoteArgs),
207
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
203
208
  }, withSync((args) => toMcp(promoteMemoryOp(args))));
204
209
  server.registerTool("memory_resolve", {
205
210
  description: TOOL_DESCRIPTIONS.memory_resolve,
206
211
  inputSchema: z.object(memoryResolveArgs),
212
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
207
213
  }, withSync((args) => toMcp(resolveMemoryOp(args))));
208
214
  server.registerTool("memory_pr_status", {
209
215
  description: TOOL_DESCRIPTIONS.memory_pr_status,
210
216
  inputSchema: z.object(memoryPrStatusArgs),
211
- annotations: { readOnlyHint: true },
217
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
218
  }, withSync((args) => toMcp(prStatusOp(args))));
213
219
  const transport = new StdioServerTransport();
214
220
  await server.connect(transport);
package/dist/tools/ops.js CHANGED
@@ -38,23 +38,23 @@ export async function withOutboxNote(scopeKey, p, reviewHint) {
38
38
  }
39
39
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
40
40
  export const TOOL_DESCRIPTIONS = {
41
- memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
42
- memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
43
- memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
44
- memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
45
- memory_forget: "Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
46
- memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
47
- memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
48
- memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
49
- memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
50
- memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
51
- memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true performs the mapped transitions locally (no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
41
+ memory_add: "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
42
+ memory_search: "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
43
+ memory_list: "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
44
+ memory_supersede: "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
45
+ memory_forget: "Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
46
+ memory_status: "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
47
+ memory_submit: "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
48
+ memory_propose: "Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
49
+ memory_promote: "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
50
+ memory_resolve: "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
51
+ memory_pr_status: "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
52
52
  };
53
53
  /** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
54
54
  export const scopeArg = z
55
- .enum(["project", "personal", "user"])
55
+ .enum(["project", "personal"])
56
56
  .optional()
57
- .describe("Memory scope. `project` = tied to this repo. `personal` = global across all your projects. `user` is a deprecated alias of `personal`. Default: project.");
57
+ .describe("Memory scope. `project` = tied to this repo. `personal` = global across all your projects. Default: project.");
58
58
  export const aliasesArg = z
59
59
  .array(z.string())
60
60
  .optional()
@@ -79,19 +79,19 @@ export const memorySearchArgs = {
79
79
  .min(1)
80
80
  .describe("Free-text query. File paths, error strings, identifiers work well."),
81
81
  scope: z
82
- .enum(["project", "personal", "user", "both"])
82
+ .enum(["project", "personal", "both"])
83
83
  .optional()
84
84
  .describe("Which scope(s) to search. Default: both."),
85
- type: z.string().optional().describe("Restrict to memories of this type."),
86
- limit: z.number().int().min(1).max(50).optional(),
85
+ type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
86
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
87
87
  };
88
88
  export const memoryListArgs = {
89
89
  scope: z
90
- .enum(["project", "personal", "user", "both"])
90
+ .enum(["project", "personal", "both"])
91
91
  .optional()
92
92
  .describe("Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project."),
93
- type: z.string().optional(),
94
- limit: z.number().int().min(1).max(100).optional(),
93
+ type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
94
+ limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
95
95
  include: z
96
96
  .enum(["active", "all"])
97
97
  .optional()
@@ -153,7 +153,7 @@ export const memoryPrStatusArgs = {
153
153
  .describe("Perform the mapped review transitions locally (no push). Default: report only."),
154
154
  };
155
155
  function resolveScope(getScope, kind) {
156
- return kind === "personal" || kind === "user" ? PERSONAL_SCOPE : getScope();
156
+ return kind === "personal" ? PERSONAL_SCOPE : getScope();
157
157
  }
158
158
  function buildFrontmatter(s, body) {
159
159
  const rfc = msToRfc3339(Date.now());
@@ -242,7 +242,7 @@ export async function addMemory(getScope, cfg, args) {
242
242
  }
243
243
  export async function searchMemories(getScope, args) {
244
244
  const project = getScope();
245
- const scopeKeys = args.scope === "personal" || args.scope === "user"
245
+ const scopeKeys = args.scope === "personal"
246
246
  ? [PERSONAL_SCOPE.key]
247
247
  : args.scope === "project"
248
248
  ? [project.key]
@@ -294,7 +294,7 @@ export async function listMemories(getScope, args) {
294
294
  }
295
295
  return { title: "memory: list (both scopes)", output: sections.join("\n\n") };
296
296
  }
297
- const s = resolveScope(getScope, args.scope === "project" || args.scope === "personal" || args.scope === "user"
297
+ const s = resolveScope(getScope, args.scope === "project" || args.scope === "personal"
298
298
  ? args.scope
299
299
  : undefined);
300
300
  const lines = render(s.key);
package/docs/V2-DESIGN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenMemex — Design Document (protocol v0.2)
2
2
 
3
- **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D74 are settled; open questions are tracked at the end of this document.
3
+ **Status:** FROZEN — protocol v0.2 (2026-09-26). Decisions D1–D76 are settled; open questions are tracked at the end of this document.
4
4
  **Author:** Stone, with 小沐
5
5
  **Changelog vs v1:** incorporates round-3 review from Perplexity, Grok, Gemini, ChatGPT, DeepSeek.
6
6
  Key changes: Design Principles section; `role` separated from `type`; two iron rules;
@@ -562,6 +562,7 @@ requirement: personal data never touches third-party services). Benchmarks to tr
562
562
  - **Phase 4 — Future, signal-gated.** Cloud `RemoteProvider` customization only on: multi-private-repo
563
563
  sharing needs, fine-grained ACL, audit/compliance mandates · optional API-backed exporters/providers
564
564
  for enterprise knowledge systems.
565
+ - **Phase 5 — Candidates recorded 2026-10-04 (not committed).** From the competitor scan (D75): an offline FTS5 retrieval eval with a published recall number (gates any semantic-layer decision) · per-client trust levels on the review ladder · cold-start Markdown import into outbox drafts · `doctor` check that each editor's MCP registration points at the intended data root (mis-rooted servers are a real failure mode elsewhere). Sequencing and scope TBD per candidate.
565
566
 
566
567
  ## 19. Migration v1 → v2
567
568
 
@@ -1589,26 +1590,27 @@ requirement: personal data never touches third-party services). Benchmarks to tr
1589
1590
  equivalence. Deliberately NOT done, pending a decision: HTML i18n and a
1590
1591
  size cap for very large stores.
1591
1592
 
1592
- ## Open Questions
1593
+ - **D75 — closed questions leave the list; competitor-inspired directions enter it (2026-10-04).** The Open Questions section is a working list, not an archive: the entries decided in D56 (host parity), D57 (retrieval explainability / memory health), D61 (checkpoint proactivity, capture criteria, retrieval layers 2–3), and D68 (memory visibility), plus the retrieval-robustness entry whose three cheap layers all shipped, are removed; the decisions remain the record. A same-day scan of five Glama-listed memory servers (auxly-memory-cli, agent-memory, remnic, Agent-Memory-Bridge, memtomem, memryzed) confirmed the current wedge — proactive in-session capture + one memory shared across editors + PR-based team sharing — appears in none of them whole. Borrowable directions are recorded instead: four new open questions (retrieval evaluation gate, per-client trust levels, cold-start import, session working state) and one roadmap candidate (`doctor` mis-registration check). Nothing here commits implementation; each direction needs its own design pass.
1593
1594
 
1594
- _Some early questions are resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull). The entries below are open unless marked decided._
1595
- - **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).
1595
+ - **D76 — tool annotations must tell the truth (2026-10-05).** `memory_pr_status` declared `readOnlyHint: true` while `apply=true` writes local review states (caught by Glama's TDQS evaluation of v0.7.2, 4.0/5). Structured hints are part of the protocol surface: all 11 tools now declare read-only / destructive / open-world hints that match their behavior, descriptions route between sibling tools instead of leaving agents to guess, and `scripts/smoke-mcp.ts` guards the pr_status combination. Description honesty is held to the same standard as behavior: ranking claims, defaults, and error semantics in tool text must match the code.
1596
1596
 
1597
- - **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.
1597
+ ## Open Questions
1598
1598
 
1599
- - **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.
1599
+ _Pruned 2026-10-04 (D75): entries decided in D56/D57/D61/D68 and the shipped layers of the retrieval-robustness question were removed — the decisions are the record. What remains below is genuinely open._
1600
1600
 
1601
1601
  - **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.
1602
1602
 
1603
1603
  - **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.
1604
1604
 
1605
- - **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.
1605
+ - **Novice experience: the 60-second magic moment and the plain-language layer (open, 2026-10-03; trimmed 2026-10-04):** the privacy model is now stated in plain words at init (D56). Still open: init offering to save one sample memory and immediately having the user's agent answer a question with it — the user *sees* recall work before being asked to trust it — and a plain-language layer over the review ladder ("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.
1606
+
1607
+ - **Retrieval evaluation: measure recall before choosing a semantic layer (open, 2026-10-04):** the retrieval-robustness question closed with all three cheap layers shipped (D56 query construction; D61 agent-side expansion + capture-time aliases), and embeddings stayed deferred *by judgment, without numbers*. A comparable project (agent-memory) publishes an offline recall@5 of 0.98 and wears it as a badge. open-memex should build the same kind of small, fixed eval — a query set over a seeded store, recall@k / MRR for the current FTS5 pipeline — and publish the number. The semantic-layer decision then gets a gate: if FTS5 + aliases is good enough, embeddings stay deferred; if a query class measurably fails, that class defines what an optional local-embedding layer must beat. Eval first, architecture second.
1606
1608
 
1607
- - **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.
1609
+ - **Per-client trust levels (open, 2026-10-04):** the review ladder is currently global: every host's writes face the same gates. A comparable tool (auxly) sets a trust level per agent — one agent's writes land immediately, another's queue for approval, a third is read-only. That maps naturally onto open-memex, since the ladder already exists; the open questions are the config surface (per host? per MCP client name? how does the server learn the client's identity reliably?), the default for unknown clients, and whether trust should also gate *reads* of the personal scope. Signal to watch: one user running several agents with different autonomy expectations.
1608
1610
 
1609
- - **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.
1611
+ - **Cold-start import: index what the user already has (open, 2026-10-04):** adoption today starts from an empty store. Comparable tools index the user's existing notes (memtomem) or import other memory systems' exports (remnic: mem0 / ChatGPT / Claude). Candidate first steps, cheapest first: (1) import a folder of plain Markdown notes as personal-scope outbox drafts — never direct saves, the review gate stays the front door; (2) named-export importers once (1) proves the pipeline. Open sub-questions: chunking policy for long notes (one file ≠ one memory), dedup against existing memories, and provenance marking (`source: import`).
1610
1612
 
1611
- - **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`.
1613
+ - **Session working state: resume where you left off (open, 2026-10-04):** open-memex stores durable knowledge, not the state of an interrupted task. memryzed's third layer — per-repository session state (open files, last commands, where the work stands) so a fresh session resumes mid-task — names the gap. Candidate shape, if pursued: an ephemeral per-project working-state note, agent-written at wrap-up and surfaced at session start alongside memory injection, *outside* the review ladder (it is scratch state, not knowledge) with a short TTL. Open questions: what belongs in it (task summary? file list? next step?), who writes it (agent at Stop? user command?), and the overlap with the retrospective-distillation and checkpoint-proactivity threads. Defer until there is signal that users lose work context, not just facts.
1612
1614
 
1613
1615
  ---
1614
1616
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  发布时照着这一页走。npm publish 和 MCP Registry 发布都在维护者本机执行。
4
4
 
5
+ > 命令一条一条跑,不要整段一起贴:`publish` 之前有几道必须确认成功的关卡,顺序错了会把半成品发出去。
6
+
5
7
  ## 同步版本号的四个地方
6
8
 
7
9
  - `package.json` 和 `package-lock.json`(同一个 commit 里一起改)
@@ -15,15 +17,27 @@
15
17
  - 纯修 bug:patch 线递增
16
18
  - 已经 push 过的版本号,内容再有变化必须 bump,不能原地改
17
19
 
18
- ## 发布前检查(在仓库根目录执行)
20
+ ## 发布前检查(在仓库根目录执行,一条一条来)
21
+
22
+ 先拉最新代码:
19
23
 
20
24
  ```powershell
21
25
  git pull
26
+ ```
27
+
28
+ 再构建:
29
+
30
+ ```powershell
22
31
  npm run build
32
+ ```
33
+
34
+ 最后跑全量测试。`test-full` 之前必须先 `build`:测试里有 dist 内容指纹守卫,没 build 会误报失败。
35
+
36
+ ```powershell
23
37
  npm run test-full
24
38
  ```
25
39
 
26
- `test-full` 之前必须先 `build`:测试里有 dist 内容指纹守卫,没 build 会误报失败。
40
+ 测试全绿再往下走,中间任何一步报错就停。
27
41
 
28
42
  ## 本机前提(Windows)
29
43
 
@@ -31,6 +45,11 @@ npm run test-full
31
45
 
32
46
  ```powershell
33
47
  nvm use 22.23.3
48
+ ```
49
+
50
+ 确认版本已经切好:
51
+
52
+ ```powershell
34
53
  node -v
35
54
  ```
36
55
 
@@ -42,8 +61,15 @@ npm install -g open-memex
42
61
 
43
62
  ## 发 alpha
44
63
 
64
+ 先确认 npm 登录态:
65
+
45
66
  ```powershell
46
67
  npm whoami
68
+ ```
69
+
70
+ 输出的是自己的 npm 用户名再发布:
71
+
72
+ ```powershell
47
73
  npm publish --tag alpha
48
74
  ```
49
75
 
@@ -52,21 +78,27 @@ npm publish --tag alpha
52
78
  ## 发 stable
53
79
 
54
80
  1. main 上的版本号落到 `X.Y.Z`(可让小沐代提交),`CHANGELOG.md` 完成分节,`server.json` 同步到同一版本。
55
- 2. 发布:
81
+ 2. 一条一条执行:先确认登录态:
56
82
 
57
83
  ```powershell
58
84
  npm whoami
85
+ ```
86
+
87
+ 再发布:
88
+
89
+ ```powershell
59
90
  npm publish
60
91
  ```
61
92
 
62
93
  3. 把版本号告诉小沐(例如"0.7.2 published"):小沐核验 npm latest,然后打 git tag `vX.Y.Z` 并建 GitHub Release。
63
- 4. 同步 MCP Registry(在仓库根目录执行;`login` 登录态过期时才需要重跑):
94
+ 4. 同步 MCP Registry。**必须等 npm 新版生效后再跑**:registry 会去 npm 抓新版本的实际包验 `mcpName` 和版本。`npm publish` 成功后先等 1–2 分钟,再执行发布(登录态过期时才需要先重跑 `login github`,也是一条一条来):
64
95
 
65
96
  ```powershell
66
- & "$env:USERPROFILE\tools\mcp-publisher\mcp-publisher.exe" login github
67
97
  & "$env:USERPROFILE\tools\mcp-publisher\mcp-publisher.exe" publish
68
98
  ```
69
99
 
100
+ 在仓库根目录执行。万一报版本相关的错,多半是 npm 元数据还没传播完,再等两分钟重跑这一条即可。
101
+
70
102
  ## 出问题先看这里
71
103
 
72
104
  - `npm publish` 失败:先确认 `npm whoami` 的登录态,再看报错第一行。
package/glama.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "stoneskin"
5
+ ]
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-memex",
3
- "version": "0.7.2",
3
+ "version": "0.7.3-alpha.2",
4
4
  "mcpName": "io.github.stoneskin/open-memex",
5
5
  "description": "Local-first memory layer and protocol for AI coding agents. Markdown source of truth, SQLite FTS5 index, zero cloud.",
6
6
  "keywords": [
@@ -85,6 +85,9 @@ try {
85
85
  JSON.stringify(["memory_add", "memory_forget", "memory_list", "memory_pr_status", "memory_promote", "memory_propose", "memory_resolve", "memory_search", "memory_status", "memory_submit", "memory_supersede"]),
86
86
  names.join(","),
87
87
  );
88
+ // D76: annotations must tell the truth — pr_status mutates with apply=true.
89
+ const prStatusTool = tools.result.tools.find((t: any) => t.name === "memory_pr_status");
90
+ check("pr_status is not read-only (D76)", prStatusTool?.annotations?.readOnlyHint === false, JSON.stringify(prStatusTool?.annotations));
88
91
  const addSchema = tools.result.tools.find((t: any) => t.name === "memory_add").inputSchema;
89
92
  check("memory_add schema has content+type+scope+tags", !!addSchema.properties?.content && !!addSchema.properties?.type, Object.keys(addSchema.properties ?? {}).join(","));
90
93
 
package/src/mcp.ts CHANGED
@@ -209,6 +209,7 @@ export async function runMcpServer() {
209
209
  {
210
210
  description: TOOL_DESCRIPTIONS.memory_add,
211
211
  inputSchema: z.object(memoryAddArgs),
212
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
212
213
  },
213
214
  withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))),
214
215
  );
@@ -218,7 +219,7 @@ export async function runMcpServer() {
218
219
  {
219
220
  description: TOOL_DESCRIPTIONS.memory_search,
220
221
  inputSchema: z.object(memorySearchArgs),
221
- annotations: { readOnlyHint: true },
222
+ annotations: { readOnlyHint: true, openWorldHint: false },
222
223
  },
223
224
  withSync((args) => toMcp(searchMemories(getScope, args))),
224
225
  );
@@ -228,7 +229,7 @@ export async function runMcpServer() {
228
229
  {
229
230
  description: TOOL_DESCRIPTIONS.memory_list,
230
231
  inputSchema: z.object(memoryListArgs),
231
- annotations: { readOnlyHint: true },
232
+ annotations: { readOnlyHint: true, openWorldHint: false },
232
233
  },
233
234
  withSync((args) => toMcp(listMemories(getScope, args))),
234
235
  );
@@ -238,6 +239,7 @@ export async function runMcpServer() {
238
239
  {
239
240
  description: TOOL_DESCRIPTIONS.memory_supersede,
240
241
  inputSchema: z.object(memorySupersedeArgs),
242
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
241
243
  },
242
244
  withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))),
243
245
  );
@@ -247,7 +249,7 @@ export async function runMcpServer() {
247
249
  {
248
250
  description: TOOL_DESCRIPTIONS.memory_forget,
249
251
  inputSchema: z.object(memoryForgetArgs),
250
- annotations: { destructiveHint: true },
252
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
251
253
  },
252
254
  withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))),
253
255
  );
@@ -257,7 +259,7 @@ export async function runMcpServer() {
257
259
  {
258
260
  description: TOOL_DESCRIPTIONS.memory_status,
259
261
  inputSchema: z.object(memoryStatusArgs),
260
- annotations: { readOnlyHint: true },
262
+ annotations: { readOnlyHint: true, openWorldHint: false },
261
263
  },
262
264
  withSync((_args) => toMcp(statusMemories())),
263
265
  );
@@ -267,6 +269,7 @@ export async function runMcpServer() {
267
269
  {
268
270
  description: TOOL_DESCRIPTIONS.memory_submit,
269
271
  inputSchema: z.object(memorySubmitArgs),
272
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
270
273
  },
271
274
  withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))),
272
275
  );
@@ -276,6 +279,7 @@ export async function runMcpServer() {
276
279
  {
277
280
  description: TOOL_DESCRIPTIONS.memory_propose,
278
281
  inputSchema: z.object(memoryProposeArgs),
282
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
279
283
  },
280
284
  withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))),
281
285
  );
@@ -285,6 +289,7 @@ export async function runMcpServer() {
285
289
  {
286
290
  description: TOOL_DESCRIPTIONS.memory_promote,
287
291
  inputSchema: z.object(memoryPromoteArgs),
292
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
288
293
  },
289
294
  withSync((args) => toMcp(promoteMemoryOp(args))),
290
295
  );
@@ -294,6 +299,7 @@ export async function runMcpServer() {
294
299
  {
295
300
  description: TOOL_DESCRIPTIONS.memory_resolve,
296
301
  inputSchema: z.object(memoryResolveArgs),
302
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
297
303
  },
298
304
  withSync((args) => toMcp(resolveMemoryOp(args))),
299
305
  );
@@ -303,7 +309,7 @@ export async function runMcpServer() {
303
309
  {
304
310
  description: TOOL_DESCRIPTIONS.memory_pr_status,
305
311
  inputSchema: z.object(memoryPrStatusArgs),
306
- annotations: { readOnlyHint: true },
312
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
307
313
  },
308
314
  withSync((args) => toMcp(prStatusOp(args))),
309
315
  );
package/src/tools/ops.ts CHANGED
@@ -66,35 +66,35 @@ export async function withOutboxNote(
66
66
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
67
67
  export const TOOL_DESCRIPTIONS = {
68
68
  memory_add:
69
- "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects.",
69
+ "Save a fact, preference, decision, or note to persistent local memory. Call this PROACTIVELY whenever the user shares something worth remembering across sessions — project conventions, tool choices, personal preferences, decisions made, error fixes and their causes. Worth saving: decisions and their reasons, preferences, conventions, gotchas, approaches tried and abandoned. Not worth saving: one-off task details or anything re-derivable from the code. Do not wait to be asked. For facts you infer yourself rather than the user stating, propose them first and save only on approval. Keep each memory to one self-contained statement; attach aliases when the parameter is available. If a note tells you the user's own wording was already stored verbatim this turn, that statement is already saved: do not retry it in a rewording — save only what that text does not contain, or use memory_supersede on the given id. Default scope is the current project; use the personal scope for facts about the user that apply across all projects. Returns the new memory's id; an identical fact already stored returns that id instead of duplicating.",
70
70
  memory_search:
71
- "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored.",
71
+ "Search persistent memory by keyword (BM25 full-text). Returns matching memories from the current project and/or personal scope. Call before asking the user about past decisions, conventions, or preferences they may have told you before. Search well: break the question into its concepts and try 2–3 phrasings per concept — synonyms, the user's other language, shorter keyword forms — and check the other scope too, before concluding nothing is stored. Results are ranked by BM25 within review tiers — reviewed (approved/published) project memories come first. `limit` caps how many come back (default 8) and `type` restricts to one memory type.",
72
72
  memory_list:
73
- "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown).",
73
+ "List memories in a scope, newest first, as a numbered inventory. Each line keeps the raw fields — [type] id=… created=… source=… — say them back to the user in plain words (source=user → 'you told me this'; inference → 'I inferred this, check me'; keyword → 'caught from your own wording'), never read the raw id aloud unless they ask. When the user asks 'what do you remember about me?', use scope=both and present the inventory conversationally. The user may point at an entry by its number ('delete #3'): numbers are only valid for the listing you just produced — re-run memory_list, read the candidate back in full, and get a confirmation before calling memory_forget (deletion is permanent). include=all is the audit view (superseded versions, retracted, archived also shown). `type` filters to one memory type; `limit` caps how many entries come back.",
74
74
  memory_supersede:
75
- "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated.",
75
+ "Replace an existing memory with a newer version. The old memory is kept as history (status: superseded) and retrieval returns the new one. Use when a saved fact becomes outdated and should be replaced rather than duplicated. Only active memories can be superseded; the new memory inherits type, tags, and aliases unless you pass replacements. An unknown id is reported back as a failure; nothing is written.",
76
76
  memory_forget:
77
- "Delete a memory by id. Use when the user asks to forget something. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
77
+ "Delete a memory by id. Use when the user asks to forget something. To replace an outdated fact with a corrected one instead of deleting it, use memory_supersede. Deletion is permanent: before deleting, restate the memory's content to the user and get a confirmation. With soft=true the memory is hidden instead (retracted: out of lists and search, file kept, one-way).",
78
78
  memory_status:
79
- "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
79
+ "Show the project memory sync pipeline: drafts waiting in the outbox (appdata), memories in the repo awaiting review or published, and any repo files not yet committed. Call this at session start, when the server reports drafts waiting for review, or when the user says 'sync memory' (or '同步记忆'); then ask the user which drafts to sync. For the memories themselves use memory_list; for PR review state use memory_pr_status. (MCP server and the `open-memex sync-status` CLI; the opencode native plugin does not expose this tool.)",
80
80
  memory_submit:
81
- "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.)",
81
+ "Move outbox drafts into the repo memory dir for review: copies the drafts in as proposed (or keeps a local approval), commits locally on the current branch, and moves the outbox originals out. Never creates a branch on its own — pass branch= only with the user's explicit approval for the full chain. Prints the push and PR commands — those need the user's explicit approval and are never run automatically. (MCP server and the `open-memex submit` CLI; the opencode native plugin does not expose this tool.) Unknown ids or a failed local commit abort the submit with the reason; nothing is published automatically.",
82
82
  memory_propose:
83
- "Copy personal memories into the project outbox as review drafts. The personal originals stay put. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
83
+ "Copy personal memories into the project outbox as review drafts. The personal originals stay put. Use this when personal knowledge should enter a project's review flow; memory_submit then moves the drafts into the repo, and memory_promote advances drafts already there. (MCP server and the `open-memex propose` CLI; the opencode native plugin does not expose this tool.)",
84
84
  memory_promote:
85
- "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
85
+ "Advance a project memory one step up the review ladder (proposed → approved → published), or reject it with a note. Rejected memories are never deleted — they can be revised and resubmitted. One step per call; an unknown id or a memory already published errors. Pass `by` to record the reviewer when it should differ from the default (e.g. approving on the strength of someone else's review). For brand-new outbox drafts, memory_submit is the entry point. (MCP server and the `open-memex promote` CLI; the opencode native plugin does not expose this tool.)",
86
86
  memory_resolve:
87
- "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
87
+ "List git-conflicted memory files, or attempt a field-level 3-way merge of one. Semantic conflicts are reported, never auto-resolved. Run it when a git pull or merge leaves memory files conflicted; the merged file is written in place (undo with git if the merge is wrong). For PR review state rather than file conflicts, use memory_pr_status. (MCP server and the `open-memex resolve` CLI; the opencode native plugin does not expose this tool.)",
88
88
  memory_pr_status:
89
- "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default; apply=true performs the mapped transitions locally (no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
89
+ "Read the current branch's GitHub PR and map its review state onto each in-repo memory: merged PR → published, PR approval → approved (approved_by = reviewer), changes-requested → suggestion only. Report by default — that mode changes nothing; apply=true writes the mapped transitions to the local files (still no push). (MCP server and the `open-memex pr-status` CLI; the opencode native plugin does not expose this tool.)",
90
90
  } as const;
91
91
 
92
92
  /** Shared zod input shapes (raw shape, not z.object — hosts wrap as needed). */
93
93
  export const scopeArg = z
94
- .enum(["project", "personal", "user"])
94
+ .enum(["project", "personal"])
95
95
  .optional()
96
96
  .describe(
97
- "Memory scope. `project` = tied to this repo. `personal` = global across all your projects. `user` is a deprecated alias of `personal`. Default: project.",
97
+ "Memory scope. `project` = tied to this repo. `personal` = global across all your projects. Default: project.",
98
98
  );
99
99
 
100
100
  export const aliasesArg = z
@@ -128,23 +128,23 @@ export const memorySearchArgs = {
128
128
  .min(1)
129
129
  .describe("Free-text query. File paths, error strings, identifiers work well."),
130
130
  scope: z
131
- .enum(["project", "personal", "user", "both"])
131
+ .enum(["project", "personal", "both"])
132
132
  .optional()
133
133
  .describe("Which scope(s) to search. Default: both."),
134
- type: z.string().optional().describe("Restrict to memories of this type."),
135
- limit: z.number().int().min(1).max(50).optional(),
134
+ type: z.string().optional().describe("Restrict to memories of this type (e.g. decision, preference, gotcha)."),
135
+ limit: z.number().int().min(1).max(50).optional().describe("Max results to return (default 8)."),
136
136
  };
137
137
  export type MemorySearchArgs = z.infer<z.ZodObject<typeof memorySearchArgs>>;
138
138
 
139
139
  export const memoryListArgs = {
140
140
  scope: z
141
- .enum(["project", "personal", "user", "both"])
141
+ .enum(["project", "personal", "both"])
142
142
  .optional()
143
143
  .describe(
144
144
  "Which scope(s) to list. `project` = tied to this repo. `personal` = global across all your projects. `both` lists each scope's newest. Default: project.",
145
145
  ),
146
- type: z.string().optional(),
147
- limit: z.number().int().min(1).max(100).optional(),
146
+ type: z.string().optional().describe("Only list memories of this type (e.g. decision, preference, gotcha)."),
147
+ limit: z.number().int().min(1).max(100).optional().describe("Max entries to list, newest first (default 20)."),
148
148
  include: z
149
149
  .enum(["active", "all"])
150
150
  .optional()
@@ -231,9 +231,9 @@ export type MemoryPrStatusArgs = z.infer<z.ZodObject<typeof memoryPrStatusArgs>>
231
231
 
232
232
  function resolveScope(
233
233
  getScope: () => Scope,
234
- kind?: "project" | "personal" | "user",
234
+ kind?: "project" | "personal",
235
235
  ): Scope {
236
- return kind === "personal" || kind === "user" ? PERSONAL_SCOPE : getScope();
236
+ return kind === "personal" ? PERSONAL_SCOPE : getScope();
237
237
  }
238
238
 
239
239
  function buildFrontmatter(
@@ -346,7 +346,7 @@ export async function searchMemories(
346
346
  ): Promise<ToolResult> {
347
347
  const project = getScope();
348
348
  const scopeKeys =
349
- args.scope === "personal" || args.scope === "user"
349
+ args.scope === "personal"
350
350
  ? [PERSONAL_SCOPE.key]
351
351
  : args.scope === "project"
352
352
  ? [project.key]
@@ -407,7 +407,7 @@ export async function listMemories(
407
407
  }
408
408
  const s = resolveScope(
409
409
  getScope,
410
- args.scope === "project" || args.scope === "personal" || args.scope === "user"
410
+ args.scope === "project" || args.scope === "personal"
411
411
  ? args.scope
412
412
  : undefined,
413
413
  );