open-memex 0.6.0 → 0.6.1-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/README.md +546 -387
- package/README.zh-CN.md +381 -447
- package/dist/cli.js +71 -7
- package/dist/doctor.js +23 -1
- package/dist/init.js +3 -0
- package/dist/retrieve/cjk.js +14 -2
- package/dist/retrieve/query.js +54 -0
- package/dist/retrieve/search.js +20 -19
- package/dist/store/audit.js +101 -0
- package/dist/store/db.js +5 -0
- package/dist/store/markdown.js +6 -1
- package/dist/tools/memory.js +9 -1
- package/dist/tools/ops.js +6 -6
- package/docs/CONCEPTS.md +181 -0
- package/docs/CONCEPTS.zh-CN.md +163 -0
- package/docs/V2-DESIGN.md +80 -0
- package/package.json +1 -1
- package/scripts/smoke-pure.ts +36 -0
- package/src/cli.ts +78 -7
- package/src/doctor.ts +23 -1
- package/src/init.ts +3 -0
- package/src/retrieve/cjk.ts +18 -2
- package/src/retrieve/query.ts +58 -0
- package/src/retrieve/search.ts +27 -19
- package/src/store/audit.ts +136 -0
- package/src/store/db.ts +5 -0
- package/src/store/markdown.ts +6 -1
- package/src/tools/memory.ts +16 -1
- package/src/tools/ops.ts +6 -6
- package/docs/USER-GUIDE.md +0 -196
- package/docs/USER-GUIDE.zh-CN.md +0 -160
package/docs/CONCEPTS.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Concepts — how open-memex works
|
|
2
|
+
|
|
3
|
+
[中文版](./CONCEPTS.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
> This document explains open-memex's **mental model**: where your memories
|
|
6
|
+
> live, how they flow, and who can see them. It is not a how-to — for
|
|
7
|
+
> installation and commands, start with the [README](../README.md). Read this
|
|
8
|
+
> when you want to predict what open-memex will do before it does it.
|
|
9
|
+
|
|
10
|
+
## In one sentence
|
|
11
|
+
|
|
12
|
+
open-memex is a local-first memory layer: **Markdown is the source of truth;
|
|
13
|
+
SQLite is just a rebuildable index.** Lose the index and you rebuild it.
|
|
14
|
+
Lose the Markdown and it's really gone.
|
|
15
|
+
|
|
16
|
+
## 1. Two scopes: personal vs project
|
|
17
|
+
|
|
18
|
+
Every memory belongs to a scope, which decides **how far it may travel**:
|
|
19
|
+
|
|
20
|
+
| | personal | project |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Holds | Facts about you: preferences, habits, cross-project info | Facts about the project: stack, conventions, decisions, gotchas |
|
|
23
|
+
| Lives | Only on your machine (see §2) | Local + eligible for team sharing (see §2, §5) |
|
|
24
|
+
| Rule | **Never leaves this machine** | Can be promoted to team knowledge |
|
|
25
|
+
|
|
26
|
+
On the opencode native plugin, keyword capture routes automatically:
|
|
27
|
+
"remember for me… / 记住我… / 我觉得… / 我喜欢…" (about me) → personal;
|
|
28
|
+
"我们决定… / 帮我们记住…" (about us) and everything else → project.
|
|
29
|
+
Everywhere else, the scope is whatever `memory_add` / `--scope` says.
|
|
30
|
+
|
|
31
|
+
## 2. Two homes: appdata vs the repo directory
|
|
32
|
+
|
|
33
|
+
| | appdata (the desk) | repo `.ai/open-memex/` (the shelf) |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Location | Windows `%APPDATA%/open-memex`, macOS/Linux `~/.local/share/open-memex` | Project root, default `.ai/open-memex/` (configurable via `memoryDir`) |
|
|
36
|
+
| Holds | `memories/personal/` personal memories; project-scope **draft outbox**; `index.db` local index | Submitted project memories (`proposed` and up) |
|
|
37
|
+
| In git? | No | Yes — git is its courier |
|
|
38
|
+
| The index? | `index.db` is rebuildable, **never committed** | No index stored; rebuilt from Markdown on demand |
|
|
39
|
+
|
|
40
|
+
The private notebook (personal) never goes on the shelf. That's an iron rule, not a setting.
|
|
41
|
+
|
|
42
|
+
Project memories start life as **drafts in the appdata outbox** — git-invisible,
|
|
43
|
+
branch-independent. Only drafts you explicitly name move to the shelf, via
|
|
44
|
+
`open-memex submit` (§5). One stage, one home: after a successful submit the
|
|
45
|
+
outbox original is gone; before it, the repo knows nothing.
|
|
46
|
+
|
|
47
|
+
## 3. First-turn injection: which 8 project + 5 personal memories get chosen
|
|
48
|
+
|
|
49
|
+
Before the agent's first turn, open-memex injects an `[OPEN-MEMEX]` context block so it
|
|
50
|
+
doesn't need a search call just to get oriented. Defaults: **8 project + 5 personal**
|
|
51
|
+
memories (`maxProjectMemories` / `maxProfileItems`, tunable).
|
|
52
|
+
|
|
53
|
+
Selection has exactly one criterion: **most recently updated first**
|
|
54
|
+
(`ORDER BY updated_at DESC`), minus retracted/archived items, with superseded
|
|
55
|
+
versions resolved away. Not "the N most important" — "the N most recently touched".
|
|
56
|
+
What you just updated is most likely relevant to what you're doing now.
|
|
57
|
+
|
|
58
|
+
Token budget: each item is squeezed to one line (≤240 chars, ~60 tokens), so 8 items
|
|
59
|
+
cost ≈ 500 tokens worst case. Beyond that, diminishing returns — the long tail is what
|
|
60
|
+
`memory_search` is for; stuffing everything into the first turn buries the signal.
|
|
61
|
+
|
|
62
|
+
Raise it: `open-memex config set maxProjectMemories 12`
|
|
63
|
+
|
|
64
|
+
## 4. Later turns: when memory gets searched again
|
|
65
|
+
|
|
66
|
+
First-turn injection happens once. After that there is **no automatic re-search** —
|
|
67
|
+
MCP is request/response; the server cannot push. Whether `memory_search` gets called
|
|
68
|
+
is entirely the model's judgment, guided by two things:
|
|
69
|
+
|
|
70
|
+
- The injected block's footer: "Use the `memory_search` tool to look up more."
|
|
71
|
+
- The instructions written by `init`: "before asking the user about something
|
|
72
|
+
they may have told you before, call `memory_search` first — try a few keyword
|
|
73
|
+
variants before giving up."
|
|
74
|
+
|
|
75
|
+
So: when the conversation touches past decisions, preferences, or conventions, the
|
|
76
|
+
model *should* search first — but nothing enforces it. This is an honest boundary of
|
|
77
|
+
the current design: on the opencode native plugin, first-turn injection is built in,
|
|
78
|
+
but later-turn recall still depends on the agent choosing to search. Over MCP there
|
|
79
|
+
is no hard session hook at all — guidance only.
|
|
80
|
+
|
|
81
|
+
## 5. How a memory becomes team knowledge
|
|
82
|
+
|
|
83
|
+
A personal observation becomes team knowledge by exactly one road —
|
|
84
|
+
**explicit promotion, never automatic sync**:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
personal idea ──propose──▶ outbox draft ──submit──▶ proposed ──┬──promote──▶ approved ──promote──▶ published/shared
|
|
88
|
+
│ │
|
|
89
|
+
--reject --reject
|
|
90
|
+
│ │ (approval withdrawn
|
|
91
|
+
▼ │ before merge)
|
|
92
|
+
rejected ──resubmit──▶ proposed
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Keep the four jobs straight — most confusion comes from mixing them up:
|
|
96
|
+
|
|
97
|
+
- **propose crosses the boundary** (personal → project outbox; the only step
|
|
98
|
+
that copies across). It *copies* one or more personal memories into the
|
|
99
|
+
outbox as review candidates, each with a new id and a `derived_from` pointer
|
|
100
|
+
back. The personal original always stays. Solo? `--local-approve` lets you
|
|
101
|
+
self-approve.
|
|
102
|
+
- **submit moves drafts into the repo** (outbox → `.ai/open-memex/`,
|
|
103
|
+
`draft → proposed`). It makes a **local** commit **on your current branch** —
|
|
104
|
+
it never creates a branch on its own, never pushes. All-or-nothing: same id
|
|
105
|
+
with different content aborts for a human to decide.
|
|
106
|
+
- **promote only flips the status label** on a file already in
|
|
107
|
+
`.ai/open-memex/` (`proposed → approved → published`, or `--reject` with a
|
|
108
|
+
note — also allowed from `approved`, withdrawing approval before merge). It
|
|
109
|
+
never moves files between directories. Every transition is appended to the
|
|
110
|
+
memory's `review_history` (who / when / from → to / why), so the trail
|
|
111
|
+
survives the PR.
|
|
112
|
+
- **git does the transport** (push, PR, merge).
|
|
113
|
+
|
|
114
|
+
Review itself needs no special UI. A memory PR contains **only memory files**,
|
|
115
|
+
reviewed and audited separately from code PRs; reviewers check "is this true?
|
|
116
|
+
is it safe to share? any secrets?" — things a code PR's CI never checks.
|
|
117
|
+
"Request changes" needs no command: while the PR is open, edit the file, commit,
|
|
118
|
+
push; the state stays `proposed` and the PR is the review mechanism.
|
|
119
|
+
|
|
120
|
+
Two helpers close the loop between GitHub and the local index:
|
|
121
|
+
|
|
122
|
+
- `open-memex pr-status [--apply]` maps the branch PR's state onto each
|
|
123
|
+
in-repo memory (merged → `published`, approved → `approved`, changes
|
|
124
|
+
requested → a suggestion only, never an auto-reject). Report by default;
|
|
125
|
+
`--apply` performs the transitions locally.
|
|
126
|
+
- `open-memex resolve [id]` lists conflicted memory files, or field-level
|
|
127
|
+
3-way-merges one. Semantic conflicts are **reported, never auto-resolved**.
|
|
128
|
+
|
|
129
|
+
**A rejection never deletes anything.** The file stays on your branch; what
|
|
130
|
+
happens next is the human's call:
|
|
131
|
+
|
|
132
|
+
1. **Accept**: close the PR and delete the branch — the file goes with it;
|
|
133
|
+
2. **Revise and resubmit**: edit, `promote --resubmit`, commit, push — review
|
|
134
|
+
continues on the same PR;
|
|
135
|
+
3. **Keep as a record**: leave it visible with a `[rejected]` tag and your
|
|
136
|
+
note, so the team can see what was considered and why not.
|
|
137
|
+
|
|
138
|
+
Finally: `list` / `search` show the review state (`[draft]` / `[proposed]` /
|
|
139
|
+
`[approved]` / `[published]` / `[rejected]`) next to project memories, and
|
|
140
|
+
retrieval ranks reviewed knowledge (`approved` / `published`) above unreviewed
|
|
141
|
+
outbox drafts — drafts stay findable, but never pose as vetted.
|
|
142
|
+
|
|
143
|
+
## 6. Sync: git is the courier, not the brain
|
|
144
|
+
|
|
145
|
+
- **Write**: `memory_add` (project scope) → writes the **appdata outbox** and
|
|
146
|
+
updates the local index. **Never touches the repo, never auto-commits,
|
|
147
|
+
never auto-pushes.**
|
|
148
|
+
- **Submit** (explicit, your call): `open-memex submit <id...>` → local commit
|
|
149
|
+
into `.ai/open-memex/`; push/PR commands are printed for you (or done by your
|
|
150
|
+
agent on your Yes).
|
|
151
|
+
- **Pull**: `open-memex pull` (always explicit, never automatic) → git fetch +
|
|
152
|
+
fast-forward → scans `.ai/open-memex/*.md` → merges into the local `index.db`
|
|
153
|
+
by file mtime. Retrieval always goes through SQLite, never walks git.
|
|
154
|
+
- **personal scope**: never syncs (§1 iron rule).
|
|
155
|
+
- **Projects without git**: keep working; project scope degrades to
|
|
156
|
+
path-keyed local memories with a clear notice. Nothing breaks.
|
|
157
|
+
|
|
158
|
+
## 7. Security model
|
|
159
|
+
|
|
160
|
+
- **Memory is data, not instructions.** Memories come back into the agent's
|
|
161
|
+
context as text to *use*, never as commands to obey — including memories
|
|
162
|
+
that arrive via sync or were written by other people. There is no automated
|
|
163
|
+
screening of shared memory; the real boundary is the human review in §5
|
|
164
|
+
("is this true? is it safe? any secrets?") plus this data/instruction
|
|
165
|
+
separation. Read shared memories the way you'd read a wiki page a stranger
|
|
166
|
+
edited.
|
|
167
|
+
- **Provenance travels with the file.** Each memory records its `source`,
|
|
168
|
+
and review-track memories carry `proposed_by` / `approved_by` plus the
|
|
169
|
+
append-only `review_history` — all inside the Markdown file itself, so the
|
|
170
|
+
trail survives branches, PRs, and exports.
|
|
171
|
+
- **Secrets are stopped at write time.** `<private>…</private>` sections are
|
|
172
|
+
stripped before saving; recognized API keys and tokens are masked in place
|
|
173
|
+
(first 4 characters kept, so you can tell which key it was) and the write
|
|
174
|
+
proceeds. Preview with `open-memex capture --dry-run`.
|
|
175
|
+
- **personal never enters git** (§1) — not by `submit`, not by `export`
|
|
176
|
+
defaults, not ever.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
*Design record: `docs/V2-DESIGN.md` (append-only decision log). Per-scope
|
|
181
|
+
details: `docs/SCOPES.md`. Reviewer convention: `docs/CURATOR.md`.*
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 概念 —— open-memex 是怎么工作的
|
|
2
|
+
|
|
3
|
+
[English](./CONCEPTS.md)
|
|
4
|
+
|
|
5
|
+
> 本文档解释 open-memex 的**心智模型**:你的记忆住在哪里、如何流动、谁能看见。
|
|
6
|
+
> 这不是操作手册——安装与命令请看 [README](../README.zh-CN.md)。当你想在
|
|
7
|
+
> open-memex 动手之前先预判它会怎么做时,读这份。
|
|
8
|
+
|
|
9
|
+
## 一句话
|
|
10
|
+
|
|
11
|
+
open-memex 是一个本地优先的记忆层:**Markdown 是事实来源;
|
|
12
|
+
SQLite 只是可重建的索引。**丢了索引可以重建。丢了 Markdown 就真的没了。
|
|
13
|
+
|
|
14
|
+
## 1. 两种作用域:personal vs project
|
|
15
|
+
|
|
16
|
+
每条记忆属于一个作用域,它决定**这条记忆最远能到哪**:
|
|
17
|
+
|
|
18
|
+
| | personal | project |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| 装什么 | 关于你的事实:偏好、习惯、跨项目信息 | 关于项目的事实:技术栈、约定、决策、坑 |
|
|
21
|
+
| 住在哪 | 只在你的机器上(见 §2) | 本地 + 可以进入团队共享(见 §2、§5) |
|
|
22
|
+
| 铁则 | **永不离开这台机器** | 可以被提升为团队知识 |
|
|
23
|
+
|
|
24
|
+
在 opencode 原生插件上,关键词捕获会自动分流:
|
|
25
|
+
"记住我… / 替我记… / 我觉得… / 我喜欢…"(关于我)→ personal;
|
|
26
|
+
"我们决定… / 帮我们记住…"(关于我们)和其余一切 → project。
|
|
27
|
+
其他接入方式下,作用域由 `memory_add` / `--scope` 明确给出。
|
|
28
|
+
|
|
29
|
+
## 2. 两个家:appdata vs 仓库目录
|
|
30
|
+
|
|
31
|
+
| | appdata(书桌) | 仓库 `.ai/open-memex/`(书架) |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 位置 | Windows `%APPDATA%/open-memex`,macOS/Linux `~/.local/share/open-memex` | 项目根目录,默认 `.ai/open-memex/`(可用 `memoryDir` 配置) |
|
|
34
|
+
| 装什么 | `memories/personal/` 个人记忆;project 作用域的 **draft 发件箱**;`index.db` 本地索引 | 已提交的 project 记忆(`proposed` 及以上) |
|
|
35
|
+
| 进 git? | 不进 | 进——git 是它的快递员 |
|
|
36
|
+
| 索引? | `index.db` 可重建,**永不提交** | 不存索引;按需从 Markdown 重建 |
|
|
37
|
+
|
|
38
|
+
私人笔记本(personal)永远不上书架。这是铁则,不是设置项。
|
|
39
|
+
|
|
40
|
+
Project 记忆出生在 **appdata 发件箱里当 draft**——git 看不见、与分支无关。
|
|
41
|
+
只有你点名的 draft 才会通过 `open-memex submit`(§5)上书架。一个阶段一个家:
|
|
42
|
+
submit 成功后发件箱原件消失;成功之前,仓库什么都不知道。
|
|
43
|
+
|
|
44
|
+
## 3. 首轮注入:8 条 project + 5 条 personal 是怎么选出来的
|
|
45
|
+
|
|
46
|
+
在助手第一轮回复前,open-memex 会注入一个 `[OPEN-MEMEX]` 上下文块,
|
|
47
|
+
让它不必先调一次搜索就能进入状态。默认 **8 条 project + 5 条 personal**
|
|
48
|
+
(`maxProjectMemories` / `maxProfileItems`,可调)。
|
|
49
|
+
|
|
50
|
+
选择标准只有一个:**按最近更新优先**
|
|
51
|
+
(`ORDER BY updated_at DESC`),去掉已撤回/归档项,旧版本被
|
|
52
|
+
supersede 掉的也不算一份。不是"最重要的 N 条",是"最近碰过的 N 条"。
|
|
53
|
+
你刚更新过的东西,最可能与你眼下在做的事有关。
|
|
54
|
+
|
|
55
|
+
Token 账:每条压成一行(≤240 字符,约 60 tokens),8 条最坏约 500 tokens。
|
|
56
|
+
再多收益递减——长尾是 `memory_search` 的活;把所有东西都塞进首轮只会淹没信号。
|
|
57
|
+
|
|
58
|
+
调大:`open-memex config set maxProjectMemories 12`
|
|
59
|
+
|
|
60
|
+
## 4. 后续轮次:什么时候会再次搜索记忆
|
|
61
|
+
|
|
62
|
+
首轮注入只发生一次。之后**没有自动重搜**——
|
|
63
|
+
MCP 是请求/响应模式,服务端没法主动推。调不调 `memory_search`
|
|
64
|
+
完全取决于模型自己的判断,靠两处引导:
|
|
65
|
+
|
|
66
|
+
- 注入块的页脚:"Use the `memory_search` tool to look up more."
|
|
67
|
+
- `init` 写入的编辑器规则:"before asking the user about something
|
|
68
|
+
they may have told you before, call `memory_search` first — try a few keyword
|
|
69
|
+
variants before giving up."
|
|
70
|
+
|
|
71
|
+
所以:当对话碰到过往决策、偏好或约定时,模型*应该*先搜——
|
|
72
|
+
但没有任何机制强制它。这是当前设计的真实边界:在 opencode 原生插件上
|
|
73
|
+
首轮注入是内建的,但后续轮次的回想仍取决于助手是否主动搜索;
|
|
74
|
+
走 MCP 时没有硬会话钩子,只有引导。
|
|
75
|
+
|
|
76
|
+
## 5. 一条记忆如何成为团队知识
|
|
77
|
+
|
|
78
|
+
个人观察变成团队知识只有一条路——
|
|
79
|
+
**显式提升,永不自动同步**:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
personal idea ──propose──▶ outbox draft ──submit──▶ proposed ──┬──promote──▶ approved ──promote──▶ published/shared
|
|
83
|
+
│ │
|
|
84
|
+
--reject --reject
|
|
85
|
+
│ │ (merge 前撤回批准)
|
|
86
|
+
▼ │
|
|
87
|
+
rejected ──resubmit──▶ proposed
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
把四个动作的分工理清——大多数困惑都来自混淆它们:
|
|
91
|
+
|
|
92
|
+
- **propose 跨界**(personal → project 发件箱;唯一一步跨作用域拷贝)。
|
|
93
|
+
它把一条或多条个人记忆*拷贝*进发件箱当评审候选,各自生成新 id、
|
|
94
|
+
`derived_from` 指回原件。原件永远保留。单人开发?`--local-approve` 自批。
|
|
95
|
+
- **submit 把 draft 搬进仓库**(发件箱 → `.ai/open-memex/`,
|
|
96
|
+
`draft → proposed`)。它在你的**当前分支**上做**本地** commit——
|
|
97
|
+
绝不自作主张建分支、绝不 push。全有全无:同 id 不同内容会中止,交人裁决。
|
|
98
|
+
- **promote 只翻状态标签**,文件已在 `.ai/open-memex/` 里
|
|
99
|
+
(`proposed → approved → published`,或 `--reject` 附说明——
|
|
100
|
+
也可从 `approved` 驳回,即 merge 前撤回批准)。它从不在目录间搬文件。
|
|
101
|
+
每次流转都追加进该记忆的 `review_history`(谁/何时/从→到/为什么),
|
|
102
|
+
轨迹熬得过 PR 关掉。
|
|
103
|
+
- **git 负责运输**(push、PR、merge)。
|
|
104
|
+
|
|
105
|
+
评审本身不需要专用 UI。一个记忆 PR **只含记忆文件**,
|
|
106
|
+
与代码 PR 分开评审审计;审稿人看的是"这是真的吗?能共享吗?有密钥吗?"——
|
|
107
|
+
这是代码 PR 的 CI 永远不查的事。"Request changes" 不需要命令:
|
|
108
|
+
PR 开着时改文件、commit、push 即可,状态保持 `proposed`,PR 就是评审机制。
|
|
109
|
+
|
|
110
|
+
两个助手把 GitHub 和本地索引的圈闭上:
|
|
111
|
+
|
|
112
|
+
- `open-memex pr-status [--apply]` 把分支 PR 的状态映射回每条仓库内记忆
|
|
113
|
+
(已合并 → `published`,已批准 → `approved`,要求修改 → 仅建议、绝不自动驳回)。
|
|
114
|
+
默认只报告;`--apply` 在本地执行这些流转。
|
|
115
|
+
- `open-memex resolve [id]` 列出冲突的记忆文件,或对某条做字段级三路合并。
|
|
116
|
+
语义冲突**只报告、绝不自动解决**。
|
|
117
|
+
|
|
118
|
+
**驳回永远不删任何东西。**文件留在你的分支上,接下来由人决定:
|
|
119
|
+
|
|
120
|
+
1. **接受**:关 PR 删分支——文件随之而去;
|
|
121
|
+
2. **修改重提**:改文件、`promote --resubmit`、commit、push——同一 PR 继续评审;
|
|
122
|
+
3. **留作记录**:留着,带 `[rejected]` 标签和你的说明,
|
|
123
|
+
让团队看见考虑过什么、为什么没过。
|
|
124
|
+
|
|
125
|
+
最后:`list` / `search` 会在 project 记忆旁标出评审状态
|
|
126
|
+
(`[draft]` / `[proposed]` / `[approved]` / `[published]` / `[rejected]`),
|
|
127
|
+
并且检索排序让已评审知识(`approved` / `published`)压过未评审的发件箱 draft——
|
|
128
|
+
draft 找得到,但绝不冒充已审。
|
|
129
|
+
|
|
130
|
+
## 6. 同步:git 是快递员,不是大脑
|
|
131
|
+
|
|
132
|
+
- **写**:`memory_add`(project 作用域)→ 写**appdata 发件箱**
|
|
133
|
+
并更新本地索引。**永不碰仓库、永不自动 commit、永不自动 push。**
|
|
134
|
+
- **提交**(显式、你说了算):`open-memex submit <id...>` → 本地 commit
|
|
135
|
+
入 `.ai/open-memex/`;push/PR 命令打出来给你看(或你的助手在你点头后代劳)。
|
|
136
|
+
- **拉**:`open-memex pull`(永远显式、永不自动)→ git fetch +
|
|
137
|
+
fast-forward → 扫 `.ai/open-memex/*.md` → 按文件 mtime 合入本地 `index.db`。
|
|
138
|
+
检索永远走 SQLite,不翻 git。
|
|
139
|
+
- **personal 作用域**:永不同步(§1 铁则)。
|
|
140
|
+
- **无 git 的项目**:照常工作;project 作用域退化为按路径索引的本地记忆,
|
|
141
|
+
并给出明确提示。什么都不会坏。
|
|
142
|
+
|
|
143
|
+
## 7. 安全模型
|
|
144
|
+
|
|
145
|
+
- **记忆是数据,不是命令。**记忆以文本形式回到助手的上下文里,
|
|
146
|
+
是*用的*,不是*听的*——包括通过同步拉来的、或别人写的记忆。
|
|
147
|
+
共享记忆没有自动化筛查;真正的边界是 §5 的人工评审
|
|
148
|
+
("这是真的吗?能共享吗?有密钥吗?")加上这条数据/指令分离。
|
|
149
|
+
读共享记忆,要像读陌生人编辑过的 wiki 页面。
|
|
150
|
+
- **出处随文件旅行。**每条记忆记着 `source`,
|
|
151
|
+
评审轨迹里的记忆还带 `proposed_by` / `approved_by`
|
|
152
|
+
和只追加的 `review_history`——全部写在 Markdown 文件自身里,
|
|
153
|
+
所以轨迹熬得过分支、PR 和导出。
|
|
154
|
+
- **密钥在写入时拦下。**`<private>…</private>` 段在保存前剥掉;
|
|
155
|
+
识别出的 API 密钥/token 就地打码(保留前 4 位,方便认出是哪把)
|
|
156
|
+
然后继续写。用 `open-memex capture --dry-run` 预览。
|
|
157
|
+
- **personal 永不进 git**(§1)——`submit` 不行、`export` 默认不行、
|
|
158
|
+
任何方式都不行。
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
*设计记录:`docs/V2-DESIGN.md`(只追加的决策日志)。按作用域的细节:
|
|
163
|
+
`docs/SCOPES.md`。审稿人约定:`docs/CURATOR.md`。*
|
package/docs/V2-DESIGN.md
CHANGED
|
@@ -1134,11 +1134,91 @@ requirement: personal data never touches third-party services). Benchmarks to tr
|
|
|
1134
1134
|
tells agents to prefer MCP tools when available. *Lesson: don't ship two
|
|
1135
1135
|
overlapping integrations for the same editor — the agent pays the
|
|
1136
1136
|
duplication cost in chatter. Reported 2026-10-01.*
|
|
1137
|
+
- **D56** — question-shaped searches and host parity, said out loud
|
|
1138
|
+
(0.6.1-alpha.1). Two-persona review (Stone 2026-10-03) converged on small,
|
|
1139
|
+
honest fixes rather than new machinery. (1) Retrieval already OR-ed terms
|
|
1140
|
+
and ranked with bm25, but fed whole questions ("how do we configure…",
|
|
1141
|
+
"中文记忆怎么检索") straight into FTS — question words matched everything
|
|
1142
|
+
and diluted ranking. Query construction now filters a small EN/CJK
|
|
1143
|
+
function-word set, dedupes, and caps terms at 24 per side, with a
|
|
1144
|
+
never-empty fallback (query.ts; partially answers the retrieval-robustness
|
|
1145
|
+
open question — layers 2 agent-side expansion / 3 capture-time aliasing
|
|
1146
|
+
remain open, embeddings stay reserved). (2) The plugin(5)/MCP(11) tool
|
|
1147
|
+
asymmetry is now stated in the six MCP-only tool descriptions themselves,
|
|
1148
|
+
with the CLI equivalent named, so an agent looking for `memory_status`
|
|
1149
|
+
inside opencode finds the answer instead of assuming breakage (the
|
|
1150
|
+
host-parity open question's "document it" option; true parity undecided).
|
|
1151
|
+
(3) `init`'s closing lines gain a one-line privacy statement at the moment
|
|
1152
|
+
of maximum doubt: memories live only on this machine, personal ones never
|
|
1153
|
+
leave it, nothing is uploaded.
|
|
1154
|
+
|
|
1155
|
+
- **D57** — explainable search, a memory audit, and the concurrency posture
|
|
1156
|
+
written down (0.6.1-alpha.2; addresses most of the retrieval-explainability
|
|
1157
|
+
/ memory-health open question and establishes the concurrent-sessions
|
|
1158
|
+
behavior it asked for). (1) `open-memex search --explain` prints the
|
|
1159
|
+
constructed FTS expression, per-hit bm25 score, and how many matches
|
|
1160
|
+
lifecycle hid (superseded vs retracted/archived), so a miss is diagnosable
|
|
1161
|
+
— bad query, stale memory, or chain-hidden — without reading the index.
|
|
1162
|
+
(2) New `open-memex audit` (read-only; doctor checks the environment, this
|
|
1163
|
+
checks the memories): near-duplicate active pairs (same ≥ 0.8 Jaccard as
|
|
1164
|
+
write-time dedup), actives untouched for 90+ days, broken supersede chains,
|
|
1165
|
+
personal files found inside the in-repo memory dir (iron-rule breach), and
|
|
1166
|
+
index/file drift. Reports only; fixes stay with `supersede` / `forget` by
|
|
1167
|
+
hand. (3) Concurrency, measured: every process opens its own connection to
|
|
1168
|
+
one WAL index; memory files are per-memory ULIDs, so concurrent writes to
|
|
1169
|
+
*different* memories never collide on disk. SQLite was the gap — no
|
|
1170
|
+
`busy_timeout` was set, so better-sqlite3 silently tolerated 5s while
|
|
1171
|
+
bun:sqlite (opencode plugin) timed out at 0 and failed instantly under any
|
|
1172
|
+
overlapping write. Now unified at `PRAGMA busy_timeout = 5000` in `db()`.
|
|
1173
|
+
Past the timeout the CLI prints one plain line ("store is busy … nothing
|
|
1174
|
+
was lost") instead of a stack trace — and nothing *is* lost: the memory
|
|
1175
|
+
file is written before the index update, so the next sync reconciles it
|
|
1176
|
+
(verified end-to-end: a write blocked 8s still lands once the lock frees).
|
|
1177
|
+
Memory file writes are tmp+rename, so a concurrent reader never sees a
|
|
1178
|
+
torn file. Same-memory concurrent edits remain last-writer-wins (accepted:
|
|
1179
|
+
different processes editing the same memory simultaneously is a human
|
|
1180
|
+
conflict, not a storage one). `doctor` reports the live locking settings.
|
|
1181
|
+
|
|
1182
|
+
- **D58** — the opencode plugin must never runtime-import the host SDK
|
|
1183
|
+
(0.6.1-alpha.3). Field report: after a global `npm i -g open-memex`, the
|
|
1184
|
+
plugin was configured in `~/.config/opencode/opencode.jsonc` but silently
|
|
1185
|
+
inactive — no memory tools, no error, nothing in opencode's log. Bisected
|
|
1186
|
+
with the real host (opencode 1.18.34) and a minimal plugin: a bare
|
|
1187
|
+
`file://` entry loads fine; adding `import { tool } from
|
|
1188
|
+
"@opencode-ai/plugin/tool"` makes opencode skip the plugin without a
|
|
1189
|
+
trace. That import only ever resolved from a source checkout (where
|
|
1190
|
+
`npm install` provides the devDependency); from a global install's
|
|
1191
|
+
location nothing provides the module and the host does not resolve it
|
|
1192
|
+
for file:// plugins. The helper is a runtime identity (`return input`),
|
|
1193
|
+
so the fix is a local same-signature stand-in in `tools/memory.ts` —
|
|
1194
|
+
types still come from the SDK (type-only imports erase at runtime), and
|
|
1195
|
+
the plugin now depends only on real runtime dependencies. Verified in
|
|
1196
|
+
the real host against a global-install-shaped tree: all five tools
|
|
1197
|
+
register. Rule going forward: plugin code imports host SDK types only;
|
|
1198
|
+
runtime imports must resolve from the package's own dependencies.
|
|
1137
1199
|
|
|
1138
1200
|
## Open Questions
|
|
1139
1201
|
|
|
1140
1202
|
_All resolved — see D10 (rename), D11 (type/role split), D12 (explicit pull)._
|
|
1141
1203
|
|
|
1204
|
+
- **Checkpoint proactivity for capture (open, 2026-10-02):** D53 retired checkpoint *polling* — the server pushes draft counts instead. But agent proactivity at task checkpoints genuinely helps knowledge capture (§3.5 distillation), so the open question is the degree: proactive enough to catch distillable moments, without nagging the user or burning tokens at every checkpoint. Candidate directions (server-pushed hints in tool results, client-side heuristics, or a middle setting) to be evaluated in a later version; default posture TBD.
|
|
1205
|
+
|
|
1206
|
+
- **Retrieval robustness: phrasing, synonyms, extra words (open, 2026-10-03):** keyword search matches word forms, not meaning — the asker phrases it differently, adds a word, or uses a synonym and the memory is missed. The current FTS5 setup (porter stemming + pre-tokenized CJK bigrams) already covers morphology and substrings; it cannot bridge semantics. Layered candidate directions, cheapest first: (1) query construction — never feed a raw sentence to FTS; extract content terms, drop stopwords, OR them and let bm25 rank, so extra words hurt ranking instead of killing the match; (2) agent-side query expansion as an explicit protocol in the skill/instructions (decompose the question into concepts, generate 2–3 synonym variants per concept, run several searches before giving up — the LLM is the synonym dictionary, no embedding model needed); (3) capture-time aliasing — when a memory is written, the agent attaches keyword/synonym aliases so future phrasings can hit it. Embeddings are the design's reserved optional capability: real semantic matching, but they require a design decision and a local model to distribute, so they stay deferred unless (1)–(3) prove insufficient. Note (3) pairs naturally with the proactive-capture question below: the same capture pass that proposes a memory can attach its aliases.
|
|
1207
|
+
|
|
1208
|
+
- **Proactive capture: agent judgment of what is worth remembering (open, 2026-10-03):** companion to the checkpoint-proactivity question above — that one asks *when* to prompt; this one asks *what qualifies*. Agent hosts with good long-term memory (e.g. Muse) get their results from explicit memorability criteria plus checkpoint consolidation, not from silent background magic; open-memex can adopt both halves. Candidate criteria to teach via the skill/instructions: decisions **and their rationale**, preferences, conventions, gotchas, and abandoned paths ("we tried X, it failed because…") are worth proposing; one-off task details and anything re-derivable from the code are not. Two postures to evaluate: propose-at-checkpoint (agent asks "worth saving?", user vetoes, then `memory_add`) versus auto-draft (agent writes outbox drafts unasked and surfaces them in a batch at the next checkpoint — drafts are git-invisible and deletable, so the cost of a wrong guess is low). Hard boundaries either way: project scope never auto-submits, and no cross-session background consolidation is possible — the plugin lives inside sessions, so consolidation happens at in-session checkpoints and wrap-up passes only. This question also decides how much aliasing (retrieval question above) the capture pass should generate.
|
|
1209
|
+
|
|
1210
|
+
- **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.
|
|
1211
|
+
|
|
1212
|
+
- **Retrospective distillation: mining host transcripts after the fact (open, 2026-10-03):** open-memex captures only in-session (keyword triggers, explicit `memory_add`) and keeps **no transcript** — so anything not captured during the conversation is gone once it ends. Hosts with durable memory (e.g. Muse) avoid this loss with two layers open-memex skipped: full transcript retention plus background distillation. The gap is architectural, not a timing detail: the raw material for a second chance does not exist on our side. But the hosts themselves already retain it — opencode persists sessions and messages in its local store; Claude Code keeps JSONL transcripts under `~/.claude/projects/`; VS Code Copilot's chat state is locked in workspace storage (readable, fragile, not a first target). Candidate direction: a "replay distillation" flow that exports a past session's transcript and has an agent mine it in a dedicated review session, with every extracted candidate landing as an **outbox draft** for human review — never a direct save. Boundaries that keep this inside the existing design: open-memex builds no transcript store of its own (borrow the host's), mining judgment is the agent's LLM work (we provide the export and the queue, not an extractor), fully local, mined drafts carry their provenance (`source` marks them as distilled, from which session). Open sub-questions: host adapters per tool (opencode store format, Claude Code JSONL), how the user picks sessions to mine (by date? by project? "everything since last sync"?), and whether the proactive-capture criteria (open question above) should be the mining rubric verbatim — likely yes.
|
|
1213
|
+
|
|
1214
|
+
- **Retrieval explainability and memory health (open, 2026-10-03):** from a heavy-user review. Search is currently a black box: when a memory is missed, there is no way to tell whether the query was bad, the memory is stale, it was ranked down by review state, or it is hiding behind a supersede chain. Candidate additions: a debug/explain view for `search` (matched terms, bm25 score, ranking effects, supersede exclusions), and a memory-side audit next to `doctor` (which today checks the environment, not the memories): near-duplicate candidates, memories untouched for N days still occupying injection slots, broken supersede chains, orphan scopes. Also undocumented and untested territory the same review surfaced: **concurrent sessions** — several agents on one machine writing the same store (simultaneous `memory_add`, reindex races, submit vs pull). File-locking/WAL behavior needs to be established, documented, and ideally guarded by a doctor check before a user finds out via a corrupt index.
|
|
1215
|
+
|
|
1216
|
+
- **Host parity: plugin vs MCP tool surface (open, 2026-10-03):** the opencode plugin exposes 5 memory tools; the MCP server exposes 11 (`memory_status`, submit/propose/promote/resolve/pr_status missing on the plugin side). A user switching hosts mid-week hits missing tools with no explanation and reasonably assumes breakage. Options: bring the plugin to parity, or document the asymmetry as intentional (CLI covers the rest) in the tool descriptions themselves so the absence reads as design rather than defect. Needs a decision; silence is the current — worst — option.
|
|
1217
|
+
|
|
1218
|
+
- **Novice experience: first success and plain language (open, 2026-10-03):** from an AI-novice review. The install ends with instructions, not an experience: candidate fix is a 60-second magic moment — init offers to save one sample memory and immediately has the user's agent answer a question using it, so the user *sees* recall work before being asked to trust it. Same screen should state the privacy model in plain words (personal never leaves this machine, no cloud, detected secrets are masked — all already true, all currently buried in docs), and the first time a suspected secret is masked, the capture path should say so. Underneath: a plain-language layer over the state machine ("saved on this computer" / "shared with the team") with outbox/submit/promote vocabulary becoming the advanced view — this is the same vocabulary wall the PM/BA question above hits from the other side.
|
|
1219
|
+
|
|
1220
|
+
- **Memory visibility and correctability (open, 2026-10-03):** also from the novice review, and arguably the trust foundation for every other feature. A non-CLI user currently cannot answer "what does my assistant remember about me?" without reading Markdown files. Candidate directions: a human-readable inventory (local HTML export or equivalent) listing memories in plain language with provenance shown the way a person asks for it ("saved 3 days ago while you were discussing deploys in project X") and one-step forget/undo from the same view. Provenance fields already exist in frontmatter; this is presentation, not schema. Without it, proactive capture (above) will read to novices as surveillance rather than help — so this question probably gates the auto-draft posture decision in the capture question.
|
|
1221
|
+
|
|
1142
1222
|
---
|
|
1143
1223
|
|
|
1144
1224
|
*End of draft v2.*
|
package/package.json
CHANGED
package/scripts/smoke-pure.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { detectKeywords } from "../src/capture/keywords.ts";
|
|
|
7
7
|
import { DEFAULT_CONFIG } from "../src/config.ts";
|
|
8
8
|
import { resolveProjectScope, resolveCwdScope, PERSONAL_SCOPE } from "../src/scope.ts";
|
|
9
9
|
import { cjkIndexText, cjkQueryExpr, hasCjk } from "../src/retrieve/cjk.ts";
|
|
10
|
+
import { toFtsQuery } from "../src/retrieve/query.ts";
|
|
10
11
|
import { contentHash, similarity, NEAR_DUP_THRESHOLD } from "../src/store/lifecycle.ts";
|
|
11
12
|
import { userMcpConfigPath, mergeServerEntry, detectInstalledClients, mergePluginEntry, opencodeGlobalConfigPath, printManualEntryHint, parseJsonConfig, removeServerEntry, removePluginEntry, removeInstructionsSection, LEGACY_PACKAGE_NAME, shouldInstallSkill } from "../src/init.ts";
|
|
12
13
|
|
|
@@ -243,6 +244,41 @@ ok(
|
|
|
243
244
|
ok("query single char", cjkQueryExpr("猫") === '"猫"', cjkQueryExpr("猫"));
|
|
244
245
|
ok("query no cjk", cjkQueryExpr("hello") === "");
|
|
245
246
|
|
|
247
|
+
console.log("== query construction ==");
|
|
248
|
+
const fq1 = toFtsQuery("how do we configure the SSO login");
|
|
249
|
+
ok(
|
|
250
|
+
"stopwords dropped",
|
|
251
|
+
!fq1.includes('"how"*') && !fq1.includes('"we"*') && !fq1.includes('"do"*'),
|
|
252
|
+
fq1,
|
|
253
|
+
);
|
|
254
|
+
ok(
|
|
255
|
+
"content terms kept",
|
|
256
|
+
fq1.includes('"configure"*') && fq1.includes('"sso"*') && fq1.includes('"login"*'),
|
|
257
|
+
fq1,
|
|
258
|
+
);
|
|
259
|
+
const fq2 = toFtsQuery("中文记忆怎么检索");
|
|
260
|
+
ok(
|
|
261
|
+
"question bigrams dropped",
|
|
262
|
+
fq2.includes('"检索"') && !fq2.includes('"怎么"'),
|
|
263
|
+
fq2,
|
|
264
|
+
);
|
|
265
|
+
ok(
|
|
266
|
+
"filter never empties",
|
|
267
|
+
toFtsQuery("what is it") !== "" && toFtsQuery("怎么") !== "",
|
|
268
|
+
`${toFtsQuery("what is it")} / ${toFtsQuery("怎么")}`,
|
|
269
|
+
);
|
|
270
|
+
ok(
|
|
271
|
+
"tokens deduped",
|
|
272
|
+
toFtsQuery("deploy deploy deploy") === '"deploy"*',
|
|
273
|
+
toFtsQuery("deploy deploy deploy"),
|
|
274
|
+
);
|
|
275
|
+
const fq5 = toFtsQuery("auth 登录 sso 检索");
|
|
276
|
+
ok(
|
|
277
|
+
"mixed query keeps both sides",
|
|
278
|
+
fq5.includes('"auth"*') && fq5.includes("{cjk}:"),
|
|
279
|
+
fq5,
|
|
280
|
+
);
|
|
281
|
+
|
|
246
282
|
console.log("== v2 times ==");
|
|
247
283
|
ok("epoch 0 → rfc3339", msToRfc3339(0) === "1970-01-01T00:00:00Z", msToRfc3339(0));
|
|
248
284
|
ok("rfc3339 round-trip", timeToMs(msToRfc3339(1758854400000)) === 1758854400000);
|
package/src/cli.ts
CHANGED
|
@@ -50,12 +50,14 @@ Examples:
|
|
|
50
50
|
|
|
51
51
|
search: `Search memories by keyword (BM25 full-text), best matches first.
|
|
52
52
|
|
|
53
|
-
Usage: open-memex search "query" [--scope project|personal|both] [--type T] [--limit N]
|
|
53
|
+
Usage: open-memex search "query" [--scope project|personal|both] [--type T] [--limit N] [--explain]
|
|
54
54
|
|
|
55
55
|
Flags:
|
|
56
|
-
--scope
|
|
57
|
-
--type
|
|
58
|
-
--limit
|
|
56
|
+
--scope project (default), personal, or both
|
|
57
|
+
--type filter by memory type
|
|
58
|
+
--limit max results
|
|
59
|
+
--explain show the FTS expression, per-hit score, and how many matches
|
|
60
|
+
lifecycle hid (superseded / retracted / archived)
|
|
59
61
|
|
|
60
62
|
Example:
|
|
61
63
|
open-memex search "deploy checklist" --scope both`,
|
|
@@ -337,6 +339,19 @@ Usage: open-memex doctor
|
|
|
337
339
|
|
|
338
340
|
Example:
|
|
339
341
|
open-memex doctor`,
|
|
342
|
+
|
|
343
|
+
audit: `Memory health audit (read-only, reports never mutates): near-duplicate
|
|
344
|
+
pairs among active memories, stale actives untouched for 90+ days, broken
|
|
345
|
+
supersede chains, personal files found inside the repo memory dir, and
|
|
346
|
+
index/file drift.
|
|
347
|
+
|
|
348
|
+
Usage: open-memex audit [--scope project|personal|both]
|
|
349
|
+
|
|
350
|
+
Flags:
|
|
351
|
+
--scope project (default), personal, or both
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
open-memex audit --scope both`,
|
|
340
355
|
};
|
|
341
356
|
|
|
342
357
|
function usage(exitCode = 1): never {
|
|
@@ -345,7 +360,7 @@ function usage(exitCode = 1): never {
|
|
|
345
360
|
Usage:
|
|
346
361
|
open-memex where
|
|
347
362
|
open-memex list [--scope project|personal] [--type T] [--limit N]
|
|
348
|
-
open-memex search "query" [--scope project|personal|both] [--type T] [--limit N]
|
|
363
|
+
open-memex search "query" [--scope project|personal|both] [--type T] [--limit N] [--explain]
|
|
349
364
|
open-memex add "content" [--scope project|personal] [--type T] [--tag t1,t2]
|
|
350
365
|
open-memex supersede <id> "new content" [--type T] [--tag t1,t2]
|
|
351
366
|
open-memex status <id> active|deprecated|retracted|archived
|
|
@@ -362,6 +377,7 @@ Usage:
|
|
|
362
377
|
open-memex submit <id...> [--branch <name>] [--base <branch>]
|
|
363
378
|
open-memex pr-status [--apply]
|
|
364
379
|
open-memex reindex
|
|
380
|
+
open-memex audit [--scope project|personal|both]
|
|
365
381
|
open-memex scopes
|
|
366
382
|
open-memex migrate [--from <key>] [--to <key>]
|
|
367
383
|
[--dry-run] [--on-conflict newer|overwrite|skip]
|
|
@@ -698,6 +714,39 @@ async function main() {
|
|
|
698
714
|
return;
|
|
699
715
|
}
|
|
700
716
|
|
|
717
|
+
if (cmd === "audit") {
|
|
718
|
+
const flags = parseFlags(rest);
|
|
719
|
+
syncScope(project.key, "cli");
|
|
720
|
+
syncScope(PERSONAL_SCOPE.key, "cli");
|
|
721
|
+
const keys =
|
|
722
|
+
flags.scope === "personal"
|
|
723
|
+
? [PERSONAL_SCOPE.key]
|
|
724
|
+
: flags.scope === "both"
|
|
725
|
+
? [project.key, PERSONAL_SCOPE.key]
|
|
726
|
+
: [project.key];
|
|
727
|
+
const { runAudit, STALE_DAYS } = await import("./store/audit.ts");
|
|
728
|
+
const r = runAudit(keys);
|
|
729
|
+
console.log(`audit: ${r.totalMemories} memories (${keys.length === 2 ? "project+personal" : keys[0] === PERSONAL_SCOPE.key ? "personal" : "project"})`);
|
|
730
|
+
console.log(` near-duplicate active pairs: ${r.duplicatePairs.length}`);
|
|
731
|
+
for (const p of r.duplicatePairs.slice(0, 10)) {
|
|
732
|
+
console.log(` ${p.a} ~ ${p.b} (similarity ${p.score.toFixed(2)}) — consider supersede`);
|
|
733
|
+
}
|
|
734
|
+
console.log(` stale actives (>= ${STALE_DAYS} days untouched): ${r.stale.length}`);
|
|
735
|
+
for (const s of r.stale.slice(0, 10)) {
|
|
736
|
+
console.log(` ${s.id} — ${s.ageDays} days`);
|
|
737
|
+
}
|
|
738
|
+
console.log(` broken supersede chains: ${r.brokenChains.length}`);
|
|
739
|
+
for (const b of r.brokenChains) {
|
|
740
|
+
console.log(` ${b.id} -> missing ${b.missingTarget}`);
|
|
741
|
+
}
|
|
742
|
+
console.log(` personal files inside repo memory dir: ${r.personalInRepo.length}`);
|
|
743
|
+
for (const f of r.personalInRepo) console.log(` ${f}`);
|
|
744
|
+
console.log(
|
|
745
|
+
` index drift: ${r.drift.filesNotIndexed} files not indexed, ${r.drift.rowsMissingFiles} rows missing files`,
|
|
746
|
+
);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
701
750
|
// `config` prints the effective configuration (defaults + file). No DB needed.
|
|
702
751
|
// `config set <key> <value>` persists a setting to the config file.
|
|
703
752
|
if (cmd === "config") {
|
|
@@ -831,11 +880,22 @@ async function main() {
|
|
|
831
880
|
: flags.scope === "project"
|
|
832
881
|
? [project.key]
|
|
833
882
|
: [project.key, PERSONAL_SCOPE.key];
|
|
883
|
+
const explain = flags.explain === "true";
|
|
884
|
+
const stats = explain
|
|
885
|
+
? { ftsQuery: "", candidates: 0, hiddenSuperseded: 0, hiddenExcluded: 0 }
|
|
886
|
+
: undefined;
|
|
834
887
|
const hits = search(query, {
|
|
835
888
|
scopeKeys: keys,
|
|
836
889
|
limit: flags.limit ? Number(flags.limit) : undefined,
|
|
837
890
|
type: flags.type,
|
|
891
|
+
stats,
|
|
838
892
|
});
|
|
893
|
+
if (explain && stats) {
|
|
894
|
+
console.log(`fts: ${stats.ftsQuery || "(empty query)"}`);
|
|
895
|
+
console.log(
|
|
896
|
+
`candidates: ${stats.candidates}, hidden by lifecycle: ${stats.hiddenSuperseded} superseded, ${stats.hiddenExcluded} retracted/archived`,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
839
899
|
if (hits.length === 0) {
|
|
840
900
|
console.log("(no matches)");
|
|
841
901
|
return;
|
|
@@ -844,7 +904,8 @@ async function main() {
|
|
|
844
904
|
const tag = h.scope_key === PERSONAL_SCOPE.key ? "personal" : "project";
|
|
845
905
|
const dep = h.status === "deprecated" ? " [deprecated]" : "";
|
|
846
906
|
const rev = hitStateLabel(h);
|
|
847
|
-
|
|
907
|
+
const sc = explain ? ` score=${h.score.toFixed(3)}` : "";
|
|
908
|
+
console.log(`[${tag}/${h.type}]${dep}${rev}${sc} ${h.id} ${h.snippet.replace(/\s+/g, " ").trim()}`);
|
|
848
909
|
}
|
|
849
910
|
return;
|
|
850
911
|
}
|
|
@@ -1361,7 +1422,17 @@ function positionalArgs(argv: string[]): string[] {
|
|
|
1361
1422
|
|
|
1362
1423
|
main()
|
|
1363
1424
|
.catch((err) => {
|
|
1364
|
-
|
|
1425
|
+
// D57: lock contention past the busy timeout should read as a transient
|
|
1426
|
+
// condition, not a stack trace. The memory file is written before the
|
|
1427
|
+
// index update, so a locked-out write still lands on the next sync.
|
|
1428
|
+
const code = (err as { code?: string })?.code;
|
|
1429
|
+
if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED") {
|
|
1430
|
+
console.error(
|
|
1431
|
+
"open-memex: the memory store is busy (another open-memex process is writing). Wait a moment and retry — nothing was lost.",
|
|
1432
|
+
);
|
|
1433
|
+
} else {
|
|
1434
|
+
console.error(err);
|
|
1435
|
+
}
|
|
1365
1436
|
process.exit(1);
|
|
1366
1437
|
})
|
|
1367
1438
|
.finally(() => {
|