memory-arbiter-mcp 0.1.0__tar.gz
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.
- memory_arbiter_mcp-0.1.0/LICENSE +21 -0
- memory_arbiter_mcp-0.1.0/PKG-INFO +242 -0
- memory_arbiter_mcp-0.1.0/README.md +225 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/__init__.py +3 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/arbitration.py +100 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/config.py +71 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/db.py +242 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/degrade.py +35 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/models.py +87 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/search.py +53 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/server.py +97 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter/tools.py +106 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/PKG-INFO +242 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/SOURCES.txt +19 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/dependency_links.txt +1 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/entry_points.txt +2 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/requires.txt +8 -0
- memory_arbiter_mcp-0.1.0/memory_arbiter_mcp.egg-info/top_level.txt +1 -0
- memory_arbiter_mcp-0.1.0/pyproject.toml +30 -0
- memory_arbiter_mcp-0.1.0/setup.cfg +4 -0
- memory_arbiter_mcp-0.1.0/tests/test_memory_arbiter.py +110 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 billy12151
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memory-arbiter-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local MCP memory arbiter with dual timeline conflict handling.
|
|
5
|
+
Author: OpenClaw Project
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: mcp>=1.2.0
|
|
11
|
+
Requires-Dist: pydantic>=2.6.0
|
|
12
|
+
Provides-Extra: vec
|
|
13
|
+
Requires-Dist: sqlite-vec>=0.1.1; extra == "vec"
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# memory-arbiter-mcp
|
|
19
|
+
|
|
20
|
+
**[中文](#中文) | [English](#english)**
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<a id="english"></a>
|
|
25
|
+
|
|
26
|
+
## English
|
|
27
|
+
|
|
28
|
+
A lightweight, fully local MCP Server that gives your AI coding tools a **shared memory store** with built-in conflict arbitration.
|
|
29
|
+
|
|
30
|
+
Every tool — ZCode, Codex, Cursor, Claude Code — has its own memory. They don't talk to each other. Memory Arbiter fixes this: one SQLite database, all tools read and write through the same MCP protocol, conflicts are resolved by structured rules (not LLM guesswork).
|
|
31
|
+
|
|
32
|
+
### Features
|
|
33
|
+
|
|
34
|
+
- **Structured memory write**: `content`, `agent_id`, `workspace`, `tags`, `source_type`, `event_time`, `ingest_time`, `confidence`, `protection_level`, and more.
|
|
35
|
+
- **Source trust levels**: `user_confirmed` > `document_extracted` > `agent_generated` > `unknown`.
|
|
36
|
+
- **Dual timeline arbitration**: resolves conflicts by user confirmation → event time → source trust → ingest time. Every decision comes with an explainable rationale.
|
|
37
|
+
- **Locked protection**: `user_confirmed` memories are automatically locked — no agent can overwrite them.
|
|
38
|
+
- **Client policy system**: per-client enable/disable, agent allow/deny lists for multi-agent governance.
|
|
39
|
+
- **Graceful degradation**: `sqlite-vec` → FTS5 → `LIKE` → JSONL backup. Never crashes.
|
|
40
|
+
- **Zero cloud, zero LLM calls**: pure local SQLite. No Postgres, Redis, or external services.
|
|
41
|
+
|
|
42
|
+
### Quick Start
|
|
43
|
+
|
|
44
|
+
**Requirements**: Python 3.11+
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Clone
|
|
48
|
+
git clone https://github.com/billy12151/memory-arbiter-mcp.git
|
|
49
|
+
cd memory-arbiter-mcp
|
|
50
|
+
|
|
51
|
+
# Setup
|
|
52
|
+
python3.11 -m venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
pip install -r requirements.txt
|
|
55
|
+
pip install -e .
|
|
56
|
+
|
|
57
|
+
# Optional: semantic recall via sqlite-vec
|
|
58
|
+
pip install '.[vec]'
|
|
59
|
+
|
|
60
|
+
# Run
|
|
61
|
+
memory-arbiter-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Connect Your Tool
|
|
65
|
+
|
|
66
|
+
Add to your tool's MCP config (see `examples/` for ready-made templates):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"memory-arbiter": {
|
|
72
|
+
"command": "/path/to/memory-arbiter-mcp/.venv/bin/memory-arbiter-mcp",
|
|
73
|
+
"env": {
|
|
74
|
+
"MEMORY_ARBITER_CLIENT": "zcode",
|
|
75
|
+
"MEMORY_ARBITER_AGENT_ID": "zcode-default",
|
|
76
|
+
"MEMORY_ARBITER_DB_PATH": "~/.local/share/memory-arbiter/memory.sqlite3"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Change `MEMORY_ARBITER_CLIENT` for each tool (`zcode`, `codex`, `cursor`, `claude-code`). All tools share the same `DB_PATH` — that's the whole point.
|
|
84
|
+
|
|
85
|
+
> ⚠️ **New session required**: MCP servers are loaded at session startup. Already-open sessions won't see the new tools. Start a fresh session after configuring.
|
|
86
|
+
|
|
87
|
+
### Client Config Locations
|
|
88
|
+
|
|
89
|
+
| Client | Config Location |
|
|
90
|
+
|---|---|
|
|
91
|
+
| ZCode | `~/.zcode/v2/` MCP config |
|
|
92
|
+
| Codex CLI | `~/.codex/` MCP config |
|
|
93
|
+
| Claude Code | `.mcp.json` in project root |
|
|
94
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
95
|
+
|
|
96
|
+
### MCP Tools
|
|
97
|
+
|
|
98
|
+
| Tool | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `memory_write` | Write a memory (`source_type=user_confirmed` auto-locks) |
|
|
101
|
+
| `memory_search` | Search memories (FTS5 → LIKE fallback) |
|
|
102
|
+
| `memory_compare` | Compare two memories, returns explanation only |
|
|
103
|
+
| `memory_arbitrate` | Arbitrate conflict, can record result (`apply=true`) |
|
|
104
|
+
| `memory_confirm` | Promote a memory to user-confirmed and locked |
|
|
105
|
+
| `memory_list_conflicts` | List unresolved conflicts |
|
|
106
|
+
| `memory_status` | Show current mode, degradation status, storage paths |
|
|
107
|
+
|
|
108
|
+
### Data Migration
|
|
109
|
+
|
|
110
|
+
Moving to a new machine? Just copy the SQLite file:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Copy the database
|
|
114
|
+
cp ~/.local/share/memory-arbiter/memory.sqlite3 /new/machine/~/.local/share/memory-arbiter/
|
|
115
|
+
|
|
116
|
+
# Reinstall the project (don't copy .venv — rebuild it)
|
|
117
|
+
python3.11 -m venv .venv
|
|
118
|
+
source .venv/bin/activate
|
|
119
|
+
pip install -e .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Testing
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python3.11 -m pip install -r requirements.txt
|
|
126
|
+
python3.11 -m pytest
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
<a id="中文"></a>
|
|
136
|
+
|
|
137
|
+
## 中文
|
|
138
|
+
|
|
139
|
+
一个轻量、完全本地运行的 MCP Server,给你的 AI 编程工具提供**共享记忆库**,内置冲突仲裁机制。
|
|
140
|
+
|
|
141
|
+
你同时用 ZCode、Codex、Cursor、Claude Code——每个工具都有各自的记忆,互不相通。Memory Arbiter 解决这个问题:一个 SQLite 数据库,所有工具通过同一个 MCP 协议读写,冲突由结构化规则仲裁,不依赖大模型。
|
|
142
|
+
|
|
143
|
+
### 核心能力
|
|
144
|
+
|
|
145
|
+
- **结构化写入**:`content`、`agent_id`、`workspace`、`tags`、`source_type`、`event_time`、`ingest_time`、`confidence`、`protection_level` 等。
|
|
146
|
+
- **来源可信度**:`user_confirmed` > `document_extracted` > `agent_generated` > `unknown`。
|
|
147
|
+
- **双时间轴仲裁**:按 用户确认 → 事件发生时间 → 来源可信度 → 录入时间 的优先级判定,输出可解释的裁决理由。
|
|
148
|
+
- **锁定保护**:`user_confirmed` 的记忆自动锁定,任何 Agent 都不能自动覆盖。
|
|
149
|
+
- **客户端策略**:按客户端启用/禁用,Agent 级别的 allow/deny 白名单控制。
|
|
150
|
+
- **逐级降级**:`sqlite-vec` → FTS5 → `LIKE` → JSONL 备份,不会崩。
|
|
151
|
+
- **零云依赖、零大模型调用**:纯本地 SQLite,不需要 Postgres、Redis 或外部服务。
|
|
152
|
+
|
|
153
|
+
### 快速开始
|
|
154
|
+
|
|
155
|
+
**要求**:Python 3.11+
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# 克隆
|
|
159
|
+
git clone https://github.com/billy12151/memory-arbiter-mcp.git
|
|
160
|
+
cd memory-arbiter-mcp
|
|
161
|
+
|
|
162
|
+
# 安装
|
|
163
|
+
python3.11 -m venv .venv
|
|
164
|
+
source .venv/bin/activate
|
|
165
|
+
pip install -r requirements.txt
|
|
166
|
+
pip install -e .
|
|
167
|
+
|
|
168
|
+
# 可选:启用语义召回增强(sqlite-vec)
|
|
169
|
+
pip install '.[vec]'
|
|
170
|
+
|
|
171
|
+
# 启动
|
|
172
|
+
memory-arbiter-mcp
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 接入工具
|
|
176
|
+
|
|
177
|
+
在你的工具的 MCP 配置中加入(完整示例见 `examples/` 目录):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"mcpServers": {
|
|
182
|
+
"memory-arbiter": {
|
|
183
|
+
"command": "/path/to/memory-arbiter-mcp/.venv/bin/memory-arbiter-mcp",
|
|
184
|
+
"env": {
|
|
185
|
+
"MEMORY_ARBITER_CLIENT": "zcode",
|
|
186
|
+
"MEMORY_ARBITER_AGENT_ID": "zcode-default",
|
|
187
|
+
"MEMORY_ARBITER_DB_PATH": "~/.local/share/memory-arbiter/memory.sqlite3"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
> 每个工具改一下 `MEMORY_ARBITER_CLIENT` 标识(`zcode`、`codex`、`cursor`、`claude-code`),共享同一个 `DB_PATH`——这就是跨工具记忆共享的关键。
|
|
195
|
+
|
|
196
|
+
> ⚠️ **需要新建会话**:MCP Server 在客户端启动时加载,已经打开的会话不会识别新添加的 Server。配置好后请新建一个会话。
|
|
197
|
+
|
|
198
|
+
### 客户端配置位置
|
|
199
|
+
|
|
200
|
+
| 客户端 | 配置文件位置 |
|
|
201
|
+
|---|---|
|
|
202
|
+
| ZCode | `~/.zcode/v2/` 下 MCP 配置 |
|
|
203
|
+
| Codex CLI | `~/.codex/` 下 MCP 配置 |
|
|
204
|
+
| Claude Code | 项目根目录 `.mcp.json` |
|
|
205
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
206
|
+
|
|
207
|
+
### MCP 工具
|
|
208
|
+
|
|
209
|
+
| 工具 | 说明 |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `memory_write` | 写入记忆(`source_type=user_confirmed` 自动锁定) |
|
|
212
|
+
| `memory_search` | 搜索记忆(FTS5 → LIKE 自动降级) |
|
|
213
|
+
| `memory_compare` | 比较两条记忆,只返回解释 |
|
|
214
|
+
| `memory_arbitrate` | 仲裁冲突,自动判定胜者(`apply=true` 时落记录) |
|
|
215
|
+
| `memory_confirm` | 用户确认某条记忆,锁定保护 |
|
|
216
|
+
| `memory_list_conflicts` | 列出未解决的冲突 |
|
|
217
|
+
| `memory_status` | 查看运行状态、模式、降级原因 |
|
|
218
|
+
|
|
219
|
+
### 数据迁移
|
|
220
|
+
|
|
221
|
+
换电脑只需拷贝一个文件:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# 拷贝数据库
|
|
225
|
+
cp ~/.local/share/memory-arbiter/memory.sqlite3 新电脑:~/.local/share/memory-arbiter/
|
|
226
|
+
|
|
227
|
+
# 重新安装项目(.venv 不要拷贝,新机器上重建)
|
|
228
|
+
python3.11 -m venv .venv
|
|
229
|
+
source .venv/bin/activate
|
|
230
|
+
pip install -e .
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 测试
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
python3.11 -m pip install -r requirements.txt
|
|
237
|
+
python3.11 -m pytest
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### License
|
|
241
|
+
|
|
242
|
+
MIT
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# memory-arbiter-mcp
|
|
2
|
+
|
|
3
|
+
**[中文](#中文) | [English](#english)**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<a id="english"></a>
|
|
8
|
+
|
|
9
|
+
## English
|
|
10
|
+
|
|
11
|
+
A lightweight, fully local MCP Server that gives your AI coding tools a **shared memory store** with built-in conflict arbitration.
|
|
12
|
+
|
|
13
|
+
Every tool — ZCode, Codex, Cursor, Claude Code — has its own memory. They don't talk to each other. Memory Arbiter fixes this: one SQLite database, all tools read and write through the same MCP protocol, conflicts are resolved by structured rules (not LLM guesswork).
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **Structured memory write**: `content`, `agent_id`, `workspace`, `tags`, `source_type`, `event_time`, `ingest_time`, `confidence`, `protection_level`, and more.
|
|
18
|
+
- **Source trust levels**: `user_confirmed` > `document_extracted` > `agent_generated` > `unknown`.
|
|
19
|
+
- **Dual timeline arbitration**: resolves conflicts by user confirmation → event time → source trust → ingest time. Every decision comes with an explainable rationale.
|
|
20
|
+
- **Locked protection**: `user_confirmed` memories are automatically locked — no agent can overwrite them.
|
|
21
|
+
- **Client policy system**: per-client enable/disable, agent allow/deny lists for multi-agent governance.
|
|
22
|
+
- **Graceful degradation**: `sqlite-vec` → FTS5 → `LIKE` → JSONL backup. Never crashes.
|
|
23
|
+
- **Zero cloud, zero LLM calls**: pure local SQLite. No Postgres, Redis, or external services.
|
|
24
|
+
|
|
25
|
+
### Quick Start
|
|
26
|
+
|
|
27
|
+
**Requirements**: Python 3.11+
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Clone
|
|
31
|
+
git clone https://github.com/billy12151/memory-arbiter-mcp.git
|
|
32
|
+
cd memory-arbiter-mcp
|
|
33
|
+
|
|
34
|
+
# Setup
|
|
35
|
+
python3.11 -m venv .venv
|
|
36
|
+
source .venv/bin/activate
|
|
37
|
+
pip install -r requirements.txt
|
|
38
|
+
pip install -e .
|
|
39
|
+
|
|
40
|
+
# Optional: semantic recall via sqlite-vec
|
|
41
|
+
pip install '.[vec]'
|
|
42
|
+
|
|
43
|
+
# Run
|
|
44
|
+
memory-arbiter-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Connect Your Tool
|
|
48
|
+
|
|
49
|
+
Add to your tool's MCP config (see `examples/` for ready-made templates):
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"memory-arbiter": {
|
|
55
|
+
"command": "/path/to/memory-arbiter-mcp/.venv/bin/memory-arbiter-mcp",
|
|
56
|
+
"env": {
|
|
57
|
+
"MEMORY_ARBITER_CLIENT": "zcode",
|
|
58
|
+
"MEMORY_ARBITER_AGENT_ID": "zcode-default",
|
|
59
|
+
"MEMORY_ARBITER_DB_PATH": "~/.local/share/memory-arbiter/memory.sqlite3"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> Change `MEMORY_ARBITER_CLIENT` for each tool (`zcode`, `codex`, `cursor`, `claude-code`). All tools share the same `DB_PATH` — that's the whole point.
|
|
67
|
+
|
|
68
|
+
> ⚠️ **New session required**: MCP servers are loaded at session startup. Already-open sessions won't see the new tools. Start a fresh session after configuring.
|
|
69
|
+
|
|
70
|
+
### Client Config Locations
|
|
71
|
+
|
|
72
|
+
| Client | Config Location |
|
|
73
|
+
|---|---|
|
|
74
|
+
| ZCode | `~/.zcode/v2/` MCP config |
|
|
75
|
+
| Codex CLI | `~/.codex/` MCP config |
|
|
76
|
+
| Claude Code | `.mcp.json` in project root |
|
|
77
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
78
|
+
|
|
79
|
+
### MCP Tools
|
|
80
|
+
|
|
81
|
+
| Tool | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `memory_write` | Write a memory (`source_type=user_confirmed` auto-locks) |
|
|
84
|
+
| `memory_search` | Search memories (FTS5 → LIKE fallback) |
|
|
85
|
+
| `memory_compare` | Compare two memories, returns explanation only |
|
|
86
|
+
| `memory_arbitrate` | Arbitrate conflict, can record result (`apply=true`) |
|
|
87
|
+
| `memory_confirm` | Promote a memory to user-confirmed and locked |
|
|
88
|
+
| `memory_list_conflicts` | List unresolved conflicts |
|
|
89
|
+
| `memory_status` | Show current mode, degradation status, storage paths |
|
|
90
|
+
|
|
91
|
+
### Data Migration
|
|
92
|
+
|
|
93
|
+
Moving to a new machine? Just copy the SQLite file:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Copy the database
|
|
97
|
+
cp ~/.local/share/memory-arbiter/memory.sqlite3 /new/machine/~/.local/share/memory-arbiter/
|
|
98
|
+
|
|
99
|
+
# Reinstall the project (don't copy .venv — rebuild it)
|
|
100
|
+
python3.11 -m venv .venv
|
|
101
|
+
source .venv/bin/activate
|
|
102
|
+
pip install -e .
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Testing
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python3.11 -m pip install -r requirements.txt
|
|
109
|
+
python3.11 -m pytest
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
<a id="中文"></a>
|
|
119
|
+
|
|
120
|
+
## 中文
|
|
121
|
+
|
|
122
|
+
一个轻量、完全本地运行的 MCP Server,给你的 AI 编程工具提供**共享记忆库**,内置冲突仲裁机制。
|
|
123
|
+
|
|
124
|
+
你同时用 ZCode、Codex、Cursor、Claude Code——每个工具都有各自的记忆,互不相通。Memory Arbiter 解决这个问题:一个 SQLite 数据库,所有工具通过同一个 MCP 协议读写,冲突由结构化规则仲裁,不依赖大模型。
|
|
125
|
+
|
|
126
|
+
### 核心能力
|
|
127
|
+
|
|
128
|
+
- **结构化写入**:`content`、`agent_id`、`workspace`、`tags`、`source_type`、`event_time`、`ingest_time`、`confidence`、`protection_level` 等。
|
|
129
|
+
- **来源可信度**:`user_confirmed` > `document_extracted` > `agent_generated` > `unknown`。
|
|
130
|
+
- **双时间轴仲裁**:按 用户确认 → 事件发生时间 → 来源可信度 → 录入时间 的优先级判定,输出可解释的裁决理由。
|
|
131
|
+
- **锁定保护**:`user_confirmed` 的记忆自动锁定,任何 Agent 都不能自动覆盖。
|
|
132
|
+
- **客户端策略**:按客户端启用/禁用,Agent 级别的 allow/deny 白名单控制。
|
|
133
|
+
- **逐级降级**:`sqlite-vec` → FTS5 → `LIKE` → JSONL 备份,不会崩。
|
|
134
|
+
- **零云依赖、零大模型调用**:纯本地 SQLite,不需要 Postgres、Redis 或外部服务。
|
|
135
|
+
|
|
136
|
+
### 快速开始
|
|
137
|
+
|
|
138
|
+
**要求**:Python 3.11+
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# 克隆
|
|
142
|
+
git clone https://github.com/billy12151/memory-arbiter-mcp.git
|
|
143
|
+
cd memory-arbiter-mcp
|
|
144
|
+
|
|
145
|
+
# 安装
|
|
146
|
+
python3.11 -m venv .venv
|
|
147
|
+
source .venv/bin/activate
|
|
148
|
+
pip install -r requirements.txt
|
|
149
|
+
pip install -e .
|
|
150
|
+
|
|
151
|
+
# 可选:启用语义召回增强(sqlite-vec)
|
|
152
|
+
pip install '.[vec]'
|
|
153
|
+
|
|
154
|
+
# 启动
|
|
155
|
+
memory-arbiter-mcp
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 接入工具
|
|
159
|
+
|
|
160
|
+
在你的工具的 MCP 配置中加入(完整示例见 `examples/` 目录):
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"mcpServers": {
|
|
165
|
+
"memory-arbiter": {
|
|
166
|
+
"command": "/path/to/memory-arbiter-mcp/.venv/bin/memory-arbiter-mcp",
|
|
167
|
+
"env": {
|
|
168
|
+
"MEMORY_ARBITER_CLIENT": "zcode",
|
|
169
|
+
"MEMORY_ARBITER_AGENT_ID": "zcode-default",
|
|
170
|
+
"MEMORY_ARBITER_DB_PATH": "~/.local/share/memory-arbiter/memory.sqlite3"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> 每个工具改一下 `MEMORY_ARBITER_CLIENT` 标识(`zcode`、`codex`、`cursor`、`claude-code`),共享同一个 `DB_PATH`——这就是跨工具记忆共享的关键。
|
|
178
|
+
|
|
179
|
+
> ⚠️ **需要新建会话**:MCP Server 在客户端启动时加载,已经打开的会话不会识别新添加的 Server。配置好后请新建一个会话。
|
|
180
|
+
|
|
181
|
+
### 客户端配置位置
|
|
182
|
+
|
|
183
|
+
| 客户端 | 配置文件位置 |
|
|
184
|
+
|---|---|
|
|
185
|
+
| ZCode | `~/.zcode/v2/` 下 MCP 配置 |
|
|
186
|
+
| Codex CLI | `~/.codex/` 下 MCP 配置 |
|
|
187
|
+
| Claude Code | 项目根目录 `.mcp.json` |
|
|
188
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
189
|
+
|
|
190
|
+
### MCP 工具
|
|
191
|
+
|
|
192
|
+
| 工具 | 说明 |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `memory_write` | 写入记忆(`source_type=user_confirmed` 自动锁定) |
|
|
195
|
+
| `memory_search` | 搜索记忆(FTS5 → LIKE 自动降级) |
|
|
196
|
+
| `memory_compare` | 比较两条记忆,只返回解释 |
|
|
197
|
+
| `memory_arbitrate` | 仲裁冲突,自动判定胜者(`apply=true` 时落记录) |
|
|
198
|
+
| `memory_confirm` | 用户确认某条记忆,锁定保护 |
|
|
199
|
+
| `memory_list_conflicts` | 列出未解决的冲突 |
|
|
200
|
+
| `memory_status` | 查看运行状态、模式、降级原因 |
|
|
201
|
+
|
|
202
|
+
### 数据迁移
|
|
203
|
+
|
|
204
|
+
换电脑只需拷贝一个文件:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# 拷贝数据库
|
|
208
|
+
cp ~/.local/share/memory-arbiter/memory.sqlite3 新电脑:~/.local/share/memory-arbiter/
|
|
209
|
+
|
|
210
|
+
# 重新安装项目(.venv 不要拷贝,新机器上重建)
|
|
211
|
+
python3.11 -m venv .venv
|
|
212
|
+
source .venv/bin/activate
|
|
213
|
+
pip install -e .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 测试
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python3.11 -m pip install -r requirements.txt
|
|
220
|
+
python3.11 -m pytest
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from .models import ProtectionLevel, SourceType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SOURCE_RANK = {
|
|
10
|
+
SourceType.USER_CONFIRMED.value: 100,
|
|
11
|
+
SourceType.DOCUMENT_EXTRACTED.value: 70,
|
|
12
|
+
SourceType.AGENT_GENERATED.value: 45,
|
|
13
|
+
SourceType.PENDING.value: 20,
|
|
14
|
+
SourceType.UNKNOWN.value: 10,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compare_memories(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
reasons: list[str] = []
|
|
20
|
+
left_protected = is_user_protected(left)
|
|
21
|
+
right_protected = is_user_protected(right)
|
|
22
|
+
if left_protected and not right_protected:
|
|
23
|
+
return decision(left, right, left, "left", ["left is user_confirmed or locked; automatic overwrite is forbidden"])
|
|
24
|
+
if right_protected and not left_protected:
|
|
25
|
+
return decision(left, right, right, "right", ["right is user_confirmed or locked; automatic overwrite is forbidden"])
|
|
26
|
+
if left_protected and right_protected:
|
|
27
|
+
return decision(left, right, None, "manual_review", ["both records are user protected; manual review required"])
|
|
28
|
+
|
|
29
|
+
left_event = parse_time(left.get("event_time"))
|
|
30
|
+
right_event = parse_time(right.get("event_time"))
|
|
31
|
+
if left_event != right_event:
|
|
32
|
+
winner = left if left_event > right_event else right
|
|
33
|
+
side = "left" if winner is left else "right"
|
|
34
|
+
reasons.append(f"{side} has newer event_time; fact occurrence time has priority")
|
|
35
|
+
return decision(left, right, winner, side, reasons)
|
|
36
|
+
|
|
37
|
+
left_source = SOURCE_RANK.get(left.get("source_type"), 0)
|
|
38
|
+
right_source = SOURCE_RANK.get(right.get("source_type"), 0)
|
|
39
|
+
if left_source != right_source:
|
|
40
|
+
winner = left if left_source > right_source else right
|
|
41
|
+
side = "left" if winner is left else "right"
|
|
42
|
+
reasons.append(f"{side} has stronger source_type")
|
|
43
|
+
return decision(left, right, winner, side, reasons)
|
|
44
|
+
|
|
45
|
+
left_conf = float(left.get("confidence") or 0)
|
|
46
|
+
right_conf = float(right.get("confidence") or 0)
|
|
47
|
+
if left_conf != right_conf:
|
|
48
|
+
winner = left if left_conf > right_conf else right
|
|
49
|
+
side = "left" if winner is left else "right"
|
|
50
|
+
reasons.append(f"{side} has higher confidence")
|
|
51
|
+
return decision(left, right, winner, side, reasons)
|
|
52
|
+
|
|
53
|
+
left_ingest = parse_time(left.get("ingest_time"))
|
|
54
|
+
right_ingest = parse_time(right.get("ingest_time"))
|
|
55
|
+
if left_ingest != right_ingest:
|
|
56
|
+
winner = left if left_ingest > right_ingest else right
|
|
57
|
+
side = "left" if winner is left else "right"
|
|
58
|
+
reasons.append(f"{side} has newer ingest_time after equal event_time/source/confidence")
|
|
59
|
+
return decision(left, right, winner, side, reasons)
|
|
60
|
+
|
|
61
|
+
return decision(left, right, None, "tie", ["records are equivalent under configured arbitration rules"])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_user_protected(record: dict[str, Any]) -> bool:
|
|
65
|
+
return record.get("source_type") == SourceType.USER_CONFIRMED.value or record.get("protection_level") in {
|
|
66
|
+
ProtectionLevel.LOCKED.value,
|
|
67
|
+
ProtectionLevel.PROTECTED.value,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_time(value: Any) -> datetime:
|
|
72
|
+
if not value:
|
|
73
|
+
return datetime.min.replace(tzinfo=timezone.utc)
|
|
74
|
+
try:
|
|
75
|
+
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
76
|
+
if parsed.tzinfo is None:
|
|
77
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
78
|
+
return parsed.astimezone(timezone.utc)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return datetime.min.replace(tzinfo=timezone.utc)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def decision(left: dict[str, Any], right: dict[str, Any], winner: Optional[dict[str, Any]], side: str, reasons: list[str]) -> dict[str, Any]:
|
|
84
|
+
loser = None
|
|
85
|
+
if winner:
|
|
86
|
+
loser = right if winner.get("id") == left.get("id") else left
|
|
87
|
+
return {
|
|
88
|
+
"winner_side": side,
|
|
89
|
+
"winner_id": winner.get("id") if winner else None,
|
|
90
|
+
"loser_id": loser.get("id") if loser else None,
|
|
91
|
+
"manual_review": side in {"manual_review", "tie"},
|
|
92
|
+
"reasons": reasons,
|
|
93
|
+
"rule_order": [
|
|
94
|
+
"user_confirmed/locked protection",
|
|
95
|
+
"event_time",
|
|
96
|
+
"source_type",
|
|
97
|
+
"confidence",
|
|
98
|
+
"ingest_time",
|
|
99
|
+
],
|
|
100
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AgentPolicy:
|
|
12
|
+
# Per-client overrides. Any client *not* listed here defaults to enabled.
|
|
13
|
+
client_defaults: dict[str, bool] = field(default_factory=dict)
|
|
14
|
+
default_enabled: bool = True
|
|
15
|
+
allow_agents: list[str] = field(default_factory=list)
|
|
16
|
+
deny_agents: list[str] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
def enabled_for(self, client: str, agent_id: str) -> bool:
|
|
19
|
+
if agent_id in self.deny_agents:
|
|
20
|
+
return False
|
|
21
|
+
if agent_id in self.allow_agents:
|
|
22
|
+
return True
|
|
23
|
+
normalized = (client or "").lower()
|
|
24
|
+
if normalized in self.client_defaults:
|
|
25
|
+
return self.client_defaults[normalized]
|
|
26
|
+
# Default-allow: any unrecognised client is enabled.
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Settings:
|
|
32
|
+
db_path: Path
|
|
33
|
+
backup_jsonl: Path
|
|
34
|
+
policy_path: Optional[Path] = None
|
|
35
|
+
client: str = "codex"
|
|
36
|
+
agent_id: str = "default"
|
|
37
|
+
workspace: str = "default"
|
|
38
|
+
enable_sqlite_vec: bool = True
|
|
39
|
+
policy: AgentPolicy = field(default_factory=AgentPolicy)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_env(cls) -> "Settings":
|
|
43
|
+
cwd = Path.cwd()
|
|
44
|
+
policy_raw = os.getenv("MEMORY_ARBITER_POLICY")
|
|
45
|
+
settings = cls(
|
|
46
|
+
db_path=Path(os.getenv("MEMORY_ARBITER_DB_PATH", cwd / "memory_arbiter.sqlite3")).expanduser(),
|
|
47
|
+
backup_jsonl=Path(os.getenv("MEMORY_ARBITER_BACKUP_JSONL", cwd / "memory_arbiter.backup.jsonl")).expanduser(),
|
|
48
|
+
policy_path=Path(policy_raw).expanduser() if policy_raw else None,
|
|
49
|
+
client=os.getenv("MEMORY_ARBITER_CLIENT", "codex"),
|
|
50
|
+
agent_id=os.getenv("MEMORY_ARBITER_AGENT_ID", "default"),
|
|
51
|
+
workspace=os.getenv("MEMORY_ARBITER_WORKSPACE", "default"),
|
|
52
|
+
enable_sqlite_vec=os.getenv("MEMORY_ARBITER_ENABLE_SQLITE_VEC", "true").lower() not in {"0", "false", "no"},
|
|
53
|
+
)
|
|
54
|
+
settings.policy = load_policy(settings.policy_path)
|
|
55
|
+
return settings
|
|
56
|
+
|
|
57
|
+
def defaults(self) -> dict[str, str]:
|
|
58
|
+
return {"agent_id": self.agent_id, "workspace": self.workspace}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_policy(path: Optional[Path]) -> AgentPolicy:
|
|
62
|
+
if not path or not path.exists():
|
|
63
|
+
return AgentPolicy()
|
|
64
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
65
|
+
raw: dict[str, Any] = json.load(fh)
|
|
66
|
+
return AgentPolicy(
|
|
67
|
+
client_defaults=dict(raw.get("client_defaults") or AgentPolicy().client_defaults),
|
|
68
|
+
default_enabled=bool(raw.get("default_enabled", True)),
|
|
69
|
+
allow_agents=list(raw.get("allow_agents") or []),
|
|
70
|
+
deny_agents=list(raw.get("deny_agents") or []),
|
|
71
|
+
)
|