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.
@@ -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,3 @@
1
+ """Memory Arbiter MCP package."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
+ )