memory-arbiter-mcp 0.1.0__py3-none-any.whl

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,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
+ )
memory_arbiter/db.py ADDED
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from pathlib import Path
6
+ from typing import Any, Optional, Tuple
7
+
8
+ from .config import Settings
9
+ from .degrade import DegradeState
10
+ from .models import MemoryRecord, utc_now_iso
11
+
12
+
13
+ class MemoryDB:
14
+ def __init__(self, settings: Settings):
15
+ self.settings = settings
16
+ self.state = DegradeState()
17
+ self.conn: Optional[sqlite3.Connection] = None
18
+ self._connect()
19
+
20
+ def _connect(self) -> None:
21
+ self.settings.db_path.parent.mkdir(parents=True, exist_ok=True)
22
+ try:
23
+ self.conn = sqlite3.connect(str(self.settings.db_path))
24
+ self.conn.row_factory = sqlite3.Row
25
+ self.conn.execute("PRAGMA journal_mode=WAL")
26
+ self.conn.execute("PRAGMA foreign_keys=ON")
27
+ self._init_schema()
28
+ self._probe_features()
29
+ except sqlite3.Error as exc:
30
+ self.conn = None
31
+ self.state.sqlite_writable = False
32
+ self.state.mode = "jsonl_backup"
33
+ self.state.jsonl_backup_active = True
34
+ self.state.warn(f"SQLite unavailable or not writable: {exc}. Using JSONL append-only backup when possible.")
35
+
36
+ def _init_schema(self) -> None:
37
+ assert self.conn is not None
38
+ self.conn.executescript(
39
+ """
40
+ CREATE TABLE IF NOT EXISTS memories (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ content TEXT NOT NULL,
43
+ agent_id TEXT NOT NULL,
44
+ workspace TEXT NOT NULL,
45
+ tags TEXT NOT NULL DEFAULT '[]',
46
+ source_type TEXT NOT NULL,
47
+ source_ref TEXT,
48
+ event_time TEXT NOT NULL,
49
+ ingest_time TEXT NOT NULL,
50
+ confidence REAL NOT NULL DEFAULT 0.5,
51
+ protection_level TEXT NOT NULL DEFAULT 'normal',
52
+ status TEXT NOT NULL DEFAULT 'active',
53
+ subject TEXT,
54
+ metadata TEXT NOT NULL DEFAULT '{}',
55
+ created_at TEXT NOT NULL
56
+ );
57
+ CREATE TABLE IF NOT EXISTS conflicts (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ left_id INTEGER NOT NULL,
60
+ right_id INTEGER NOT NULL,
61
+ subject TEXT,
62
+ status TEXT NOT NULL DEFAULT 'open',
63
+ reason TEXT NOT NULL,
64
+ winner_id INTEGER,
65
+ created_at TEXT NOT NULL,
66
+ resolved_at TEXT,
67
+ FOREIGN KEY(left_id) REFERENCES memories(id),
68
+ FOREIGN KEY(right_id) REFERENCES memories(id)
69
+ );
70
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(workspace, agent_id, status);
71
+ CREATE INDEX IF NOT EXISTS idx_memories_subject ON memories(workspace, subject);
72
+ CREATE INDEX IF NOT EXISTS idx_memories_event ON memories(event_time, ingest_time);
73
+ """
74
+ )
75
+ self.conn.commit()
76
+
77
+ def _probe_features(self) -> None:
78
+ assert self.conn is not None
79
+ if self.settings.enable_sqlite_vec:
80
+ try:
81
+ import sqlite_vec # type: ignore
82
+
83
+ self.conn.enable_load_extension(True)
84
+ sqlite_vec.load(self.conn)
85
+ self.state.sqlite_vec_available = True
86
+ self.state.mode = "sqlite_vec"
87
+ except Exception as exc: # pragma: no cover - depends on local optional package
88
+ self.state.warn(f"sqlite-vec unavailable: {exc}. Semantic recall disabled; falling back to FTS5 or keyword search.")
89
+ else:
90
+ self.state.warn("sqlite-vec disabled by configuration. Semantic recall disabled.")
91
+
92
+ try:
93
+ self.conn.execute(
94
+ "CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(content, tags, subject, content='memories', content_rowid='id')"
95
+ )
96
+ self._rebuild_fts()
97
+ self.state.fts5_available = True
98
+ if not self.state.sqlite_vec_available:
99
+ self.state.mode = "fts5"
100
+ except sqlite3.Error as exc:
101
+ self.state.warn(f"SQLite FTS5 unavailable: {exc}. Falling back to LIKE/keyword search.")
102
+ if not self.state.sqlite_vec_available:
103
+ self.state.mode = "like"
104
+
105
+ try:
106
+ self.conn.execute("CREATE TABLE IF NOT EXISTS write_probe (id INTEGER)")
107
+ self.conn.execute("INSERT INTO write_probe(id) VALUES (1)")
108
+ self.conn.execute("DELETE FROM write_probe")
109
+ self.conn.commit()
110
+ except sqlite3.Error as exc:
111
+ self.state.sqlite_writable = False
112
+ self.state.mode = "jsonl_backup"
113
+ self.state.jsonl_backup_active = True
114
+ self.state.warn(f"SQLite opened read-only or write probe failed: {exc}. Writes will use JSONL backup when possible.")
115
+
116
+ def _rebuild_fts(self) -> None:
117
+ assert self.conn is not None
118
+ try:
119
+ self.conn.execute("INSERT INTO memories_fts(memories_fts) VALUES ('rebuild')")
120
+ self.conn.commit()
121
+ except sqlite3.Error:
122
+ self.conn.rollback()
123
+
124
+ def insert_memory(self, record: MemoryRecord) -> Tuple[Optional[int], list[str]]:
125
+ warnings: list[str] = []
126
+ if not record.content:
127
+ raise ValueError("content is required")
128
+ if self.conn is None or not self.state.sqlite_writable:
129
+ self._append_backup(record)
130
+ warnings.append("SQLite write unavailable; wrote append-only JSONL backup.")
131
+ return None, warnings
132
+ cur = self.conn.execute(
133
+ """
134
+ INSERT INTO memories
135
+ (content, agent_id, workspace, tags, source_type, source_ref, event_time, ingest_time,
136
+ confidence, protection_level, status, subject, metadata, created_at)
137
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
138
+ """,
139
+ (
140
+ record.content,
141
+ record.agent_id,
142
+ record.workspace,
143
+ json.dumps(record.tags, ensure_ascii=False),
144
+ record.source_type,
145
+ record.source_ref,
146
+ record.event_time,
147
+ record.ingest_time,
148
+ record.confidence,
149
+ record.protection_level,
150
+ record.status,
151
+ record.subject,
152
+ json.dumps(record.metadata, ensure_ascii=False),
153
+ utc_now_iso(),
154
+ ),
155
+ )
156
+ memory_id = int(cur.lastrowid)
157
+ if self.state.fts5_available:
158
+ self.conn.execute(
159
+ "INSERT INTO memories_fts(rowid, content, tags, subject) VALUES (?, ?, ?, ?)",
160
+ (memory_id, record.content, " ".join(record.tags), record.subject or ""),
161
+ )
162
+ self.conn.commit()
163
+ return memory_id, warnings
164
+
165
+ def _append_backup(self, record: MemoryRecord) -> None:
166
+ self.settings.backup_jsonl.parent.mkdir(parents=True, exist_ok=True)
167
+ payload = record.__dict__.copy()
168
+ payload["backup_written_at"] = utc_now_iso()
169
+ with self.settings.backup_jsonl.open("a", encoding="utf-8") as fh:
170
+ fh.write(json.dumps(payload, ensure_ascii=False) + "\n")
171
+ self.state.jsonl_backup_active = True
172
+
173
+ def get_memory(self, memory_id: int) -> Optional[dict[str, Any]]:
174
+ if self.conn is None:
175
+ return None
176
+ row = self.conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
177
+ return row_to_dict(row) if row else None
178
+
179
+ def update_memory(self, memory_id: int, updates: dict[str, Any]) -> bool:
180
+ if self.conn is None or not self.state.sqlite_writable:
181
+ return False
182
+ allowed = {"source_type", "confidence", "protection_level", "status", "metadata"}
183
+ pairs = [(key, value) for key, value in updates.items() if key in allowed]
184
+ if not pairs:
185
+ return True
186
+ sql = ", ".join(f"{key} = ?" for key, _ in pairs)
187
+ values = [json.dumps(v, ensure_ascii=False) if isinstance(v, (dict, list)) else v for _, v in pairs]
188
+ values.append(memory_id)
189
+ self.conn.execute(f"UPDATE memories SET {sql} WHERE id = ?", values)
190
+ self.conn.commit()
191
+ return True
192
+
193
+ def list_memories(self, workspace: Optional[str] = None, subject: Optional[str] = None, limit: int = 50) -> list[dict[str, Any]]:
194
+ if self.conn is None:
195
+ return []
196
+ clauses = ["status != 'deleted'"]
197
+ params: list[Any] = []
198
+ if workspace:
199
+ clauses.append("workspace = ?")
200
+ params.append(workspace)
201
+ if subject:
202
+ clauses.append("subject = ?")
203
+ params.append(subject)
204
+ params.append(limit)
205
+ rows = self.conn.execute(
206
+ f"SELECT * FROM memories WHERE {' AND '.join(clauses)} ORDER BY event_time DESC, ingest_time DESC LIMIT ?",
207
+ params,
208
+ ).fetchall()
209
+ return [row_to_dict(row) for row in rows]
210
+
211
+ def record_conflict(self, left_id: int, right_id: int, subject: Optional[str], reason: str, winner_id: Optional[int]) -> Optional[int]:
212
+ if self.conn is None or not self.state.sqlite_writable:
213
+ return None
214
+ cur = self.conn.execute(
215
+ """
216
+ INSERT INTO conflicts(left_id, right_id, subject, reason, winner_id, created_at)
217
+ VALUES (?, ?, ?, ?, ?, ?)
218
+ """,
219
+ (left_id, right_id, subject, reason, winner_id, utc_now_iso()),
220
+ )
221
+ self.conn.commit()
222
+ return int(cur.lastrowid)
223
+
224
+ def list_conflicts(self, status: str = "open", limit: int = 50) -> list[dict[str, Any]]:
225
+ if self.conn is None:
226
+ return []
227
+ rows = self.conn.execute(
228
+ "SELECT * FROM conflicts WHERE status = ? ORDER BY created_at DESC LIMIT ?",
229
+ (status, limit),
230
+ ).fetchall()
231
+ return [row_to_dict(row) for row in rows]
232
+
233
+
234
+ def row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
235
+ data = dict(row)
236
+ for key in ("tags", "metadata"):
237
+ if key in data and isinstance(data[key], str):
238
+ try:
239
+ data[key] = json.loads(data[key])
240
+ except json.JSONDecodeError:
241
+ pass
242
+ return data
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Optional
5
+
6
+
7
+ @dataclass
8
+ class DegradeState:
9
+ mode: str = "sqlite"
10
+ warnings: list[str] = field(default_factory=list)
11
+ sqlite_vec_available: bool = False
12
+ fts5_available: bool = False
13
+ sqlite_writable: bool = True
14
+ jsonl_backup_active: bool = False
15
+
16
+ @property
17
+ def degraded(self) -> bool:
18
+ return bool(self.warnings) or self.mode != "sqlite_vec"
19
+
20
+ def warn(self, message: str) -> None:
21
+ if message not in self.warnings:
22
+ self.warnings.append(message)
23
+
24
+ def response(self, data: Any, ok: bool = True, extra_warnings: Optional[list[str]] = None) -> dict[str, Any]:
25
+ warnings = list(self.warnings)
26
+ for warning in extra_warnings or []:
27
+ if warning not in warnings:
28
+ warnings.append(warning)
29
+ return {
30
+ "ok": ok,
31
+ "mode": self.mode,
32
+ "warnings": warnings,
33
+ "degraded": bool(warnings) or self.mode != "sqlite_vec",
34
+ "data": data,
35
+ }
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+ from typing import Any, Optional
7
+
8
+
9
+ class SourceType(str, Enum):
10
+ USER_CONFIRMED = "user_confirmed"
11
+ DOCUMENT_EXTRACTED = "document_extracted"
12
+ AGENT_GENERATED = "agent_generated"
13
+ UNKNOWN = "unknown"
14
+ PENDING = "pending"
15
+
16
+
17
+ class ProtectionLevel(str, Enum):
18
+ NORMAL = "normal"
19
+ PROTECTED = "protected"
20
+ LOCKED = "locked"
21
+
22
+
23
+ class MemoryStatus(str, Enum):
24
+ ACTIVE = "active"
25
+ SUPERSEDED = "superseded"
26
+ CONFLICTED = "conflicted"
27
+ PENDING = "pending"
28
+ DELETED = "deleted"
29
+
30
+
31
+ def utc_now_iso() -> str:
32
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
33
+
34
+
35
+ def normalize_iso(value: Optional[str]) -> str:
36
+ if not value:
37
+ return utc_now_iso()
38
+ try:
39
+ normalized = value.replace("Z", "+00:00")
40
+ parsed = datetime.fromisoformat(normalized)
41
+ if parsed.tzinfo is None:
42
+ parsed = parsed.replace(tzinfo=timezone.utc)
43
+ return parsed.astimezone(timezone.utc).replace(microsecond=0).isoformat()
44
+ except ValueError:
45
+ return value
46
+
47
+
48
+ @dataclass
49
+ class MemoryRecord:
50
+ content: str
51
+ agent_id: str
52
+ workspace: str
53
+ tags: list[str] = field(default_factory=list)
54
+ source_type: str = SourceType.UNKNOWN.value
55
+ source_ref: Optional[str] = None
56
+ event_time: str = field(default_factory=utc_now_iso)
57
+ ingest_time: str = field(default_factory=utc_now_iso)
58
+ confidence: float = 0.5
59
+ protection_level: str = ProtectionLevel.NORMAL.value
60
+ status: str = MemoryStatus.ACTIVE.value
61
+ subject: Optional[str] = None
62
+ metadata: dict[str, Any] = field(default_factory=dict)
63
+ id: Optional[int] = None
64
+
65
+ @classmethod
66
+ def from_input(cls, payload: dict[str, Any], defaults: dict[str, str]) -> "MemoryRecord":
67
+ source_type = payload.get("source_type") or SourceType.UNKNOWN.value
68
+ protection = payload.get("protection_level") or ProtectionLevel.NORMAL.value
69
+ status = payload.get("status") or MemoryStatus.ACTIVE.value
70
+ if source_type == SourceType.USER_CONFIRMED.value:
71
+ protection = ProtectionLevel.LOCKED.value
72
+ status = MemoryStatus.ACTIVE.value
73
+ return cls(
74
+ content=str(payload["content"]).strip(),
75
+ agent_id=str(payload.get("agent_id") or defaults.get("agent_id") or "default"),
76
+ workspace=str(payload.get("workspace") or defaults.get("workspace") or "default"),
77
+ tags=list(payload.get("tags") or []),
78
+ source_type=str(source_type),
79
+ source_ref=payload.get("source_ref"),
80
+ event_time=normalize_iso(payload.get("event_time")),
81
+ ingest_time=normalize_iso(payload.get("ingest_time")),
82
+ confidence=float(payload.get("confidence", 0.5)),
83
+ protection_level=str(protection),
84
+ status=str(status),
85
+ subject=payload.get("subject"),
86
+ metadata=dict(payload.get("metadata") or {}),
87
+ )
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, Tuple
4
+
5
+ from .db import MemoryDB, row_to_dict
6
+
7
+
8
+ def search_memories(db: MemoryDB, query: str, workspace: Optional[str] = None, tags: Optional[list[str]] = None, limit: int = 10) -> Tuple[list[dict[str, Any]], list[str]]:
9
+ warnings: list[str] = []
10
+ if db.conn is None:
11
+ return [], ["SQLite unavailable; search cannot read JSONL backup in MVP."]
12
+ limit = max(1, min(int(limit), 100))
13
+ query = (query or "").strip()
14
+ rows = []
15
+ if db.state.fts5_available and query:
16
+ sql = """
17
+ SELECT m.*, bm25(memories_fts) AS score
18
+ FROM memories_fts
19
+ JOIN memories m ON memories_fts.rowid = m.id
20
+ WHERE memories_fts MATCH ? AND m.status != 'deleted'
21
+ """
22
+ params: list[Any] = [query]
23
+ if workspace:
24
+ sql += " AND m.workspace = ?"
25
+ params.append(workspace)
26
+ sql += " ORDER BY score LIMIT ?"
27
+ params.append(limit)
28
+ try:
29
+ rows = db.conn.execute(sql, params).fetchall()
30
+ except Exception as exc:
31
+ warnings.append(f"FTS5 query failed: {exc}. Falling back to LIKE search.")
32
+ rows = []
33
+ if not rows:
34
+ like = f"%{query}%"
35
+ clauses = ["status != 'deleted'"]
36
+ params = []
37
+ if query:
38
+ clauses.append("(content LIKE ? OR subject LIKE ? OR tags LIKE ?)")
39
+ params.extend([like, like, like])
40
+ if workspace:
41
+ clauses.append("workspace = ?")
42
+ params.append(workspace)
43
+ for tag in tags or []:
44
+ clauses.append("tags LIKE ?")
45
+ params.append(f"%{tag}%")
46
+ params.append(limit)
47
+ rows = db.conn.execute(
48
+ f"SELECT *, 0 AS score FROM memories WHERE {' AND '.join(clauses)} ORDER BY event_time DESC, ingest_time DESC LIMIT ?",
49
+ params,
50
+ ).fetchall()
51
+ if query and not db.state.fts5_available:
52
+ warnings.append("Using LIKE/keyword search because sqlite-vec and FTS5 are unavailable.")
53
+ return [row_to_dict(row) for row in rows], warnings
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import Any, Optional
5
+
6
+ from .config import Settings
7
+ from .tools import MemoryTools
8
+
9
+
10
+ def build_server() -> Any:
11
+ try:
12
+ from mcp.server.fastmcp import FastMCP
13
+ except Exception as exc:
14
+ raise RuntimeError(
15
+ "MCP Python SDK is not installed. Install with `pip install -r requirements.txt` "
16
+ "or `pip install mcp`, then run `memory-arbiter-mcp` again."
17
+ ) from exc
18
+
19
+ app = FastMCP("memory-arbiter-mcp")
20
+ tools = MemoryTools(Settings.from_env())
21
+
22
+ @app.tool()
23
+ def memory_write(
24
+ content: str,
25
+ agent_id: Optional[str] = None,
26
+ workspace: Optional[str] = None,
27
+ tags: Optional[list[str]] = None,
28
+ source_type: str = "unknown",
29
+ source_ref: Optional[str] = None,
30
+ event_time: Optional[str] = None,
31
+ ingest_time: Optional[str] = None,
32
+ confidence: float = 0.5,
33
+ protection_level: str = "normal",
34
+ status: str = "active",
35
+ subject: Optional[str] = None,
36
+ metadata: Optional[dict[str, Any]] = None,
37
+ ) -> dict[str, Any]:
38
+ """写入一条结构化记忆到跨工具共享记忆库。必填 content(正文),建议填 subject(标题)、tags(标签)、source_type(来源类型:agent_generated/user_confirmed/document_extracted)。"""
39
+ return tools.memory_write(
40
+ content=content,
41
+ agent_id=agent_id,
42
+ workspace=workspace,
43
+ tags=tags or [],
44
+ source_type=source_type,
45
+ source_ref=source_ref,
46
+ event_time=event_time,
47
+ ingest_time=ingest_time,
48
+ confidence=confidence,
49
+ protection_level=protection_level,
50
+ status=status,
51
+ subject=subject,
52
+ metadata=metadata or {},
53
+ )
54
+
55
+ @app.tool()
56
+ def memory_search(query: str = "", workspace: Optional[str] = None, tags: Optional[list[str]] = None, limit: int = 10) -> dict[str, Any]:
57
+ """按关键词搜索跨工具共享记忆库中的已有记忆。开始新任务前先搜一下,避免重复记录。"""
58
+ return tools.memory_search(query=query, workspace=workspace, tags=tags or [], limit=limit)
59
+
60
+ @app.tool()
61
+ def memory_compare(left_id: int, right_id: int) -> dict[str, Any]:
62
+ """比较两条记忆是否冲突,返回可解释的比较理由,不落冲突记录。"""
63
+ return tools.memory_compare(left_id=left_id, right_id=right_id)
64
+
65
+ @app.tool()
66
+ def memory_arbitrate(left_id: int, right_id: int, mark_conflict: bool = True, apply: bool = False) -> dict[str, Any]:
67
+ """仲裁两条冲突记忆的胜者与败者。mark_conflict=true 记录冲突,apply=true 自动将非保护败方标记为 superseded。"""
68
+ return tools.memory_arbitrate(left_id=left_id, right_id=right_id, mark_conflict=mark_conflict, apply=apply)
69
+
70
+ @app.tool()
71
+ def memory_list_conflicts(status: str = "open", limit: int = 50) -> dict[str, Any]:
72
+ """列出记忆冲突记录,默认只看 open 状态。"""
73
+ return tools.memory_list_conflicts(status=status, limit=limit)
74
+
75
+ @app.tool()
76
+ def memory_confirm(memory_id: int, source_ref: Optional[str] = None, confidence: float = 1.0) -> dict[str, Any]:
77
+ """将一条记忆标记为用户确认,提升为 user_confirmed + locked 保护级别,禁止自动覆盖。"""
78
+ return tools.memory_confirm(memory_id=memory_id, source_ref=source_ref, confidence=confidence)
79
+
80
+ @app.tool()
81
+ def memory_status() -> dict[str, Any]:
82
+ """查看 memory-arbiter 运行状态:数据库路径、降级模式、客户端标识、策略配置。"""
83
+ return tools.memory_status()
84
+
85
+ return app
86
+
87
+
88
+ def main() -> None:
89
+ try:
90
+ build_server().run()
91
+ except RuntimeError as exc:
92
+ print(str(exc), file=sys.stderr)
93
+ raise SystemExit(2)
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, Tuple
4
+
5
+ from .arbitration import compare_memories
6
+ from .config import Settings
7
+ from .db import MemoryDB
8
+ from .models import MemoryRecord, ProtectionLevel, SourceType
9
+ from .search import search_memories
10
+
11
+
12
+ class MemoryTools:
13
+ def __init__(self, settings: Optional[Settings] = None, db: Optional[MemoryDB] = None):
14
+ self.settings = settings or Settings.from_env()
15
+ self.db = db or MemoryDB(self.settings)
16
+
17
+ def _allowed(self, agent_id: Optional[str] = None, client: Optional[str] = None) -> Tuple[bool, list[str]]:
18
+ actual_agent = agent_id or self.settings.agent_id
19
+ actual_client = client or self.settings.client
20
+ if self.settings.policy.enabled_for(actual_client, actual_agent):
21
+ return True, []
22
+ return False, [f"Memory arbiter disabled by policy for client={actual_client}, agent_id={actual_agent}."]
23
+
24
+ def memory_write(self, **payload: Any) -> dict[str, Any]:
25
+ allowed, warnings = self._allowed(payload.get("agent_id"), payload.get("client"))
26
+ if not allowed:
27
+ return self.db.state.response({"written": False}, ok=False, extra_warnings=warnings)
28
+ try:
29
+ record = MemoryRecord.from_input(payload, self.settings.defaults())
30
+ memory_id, write_warnings = self.db.insert_memory(record)
31
+ data = {"id": memory_id, "backup_only": memory_id is None, "record": {**record.__dict__, "id": memory_id}}
32
+ return self.db.state.response(data, extra_warnings=warnings + write_warnings)
33
+ except Exception as exc:
34
+ return self.db.state.response({"error": str(exc)}, ok=False, extra_warnings=warnings)
35
+
36
+ def memory_search(self, query: str = "", workspace: Optional[str] = None, tags: Optional[list[str]] = None, limit: int = 10, **_: Any) -> dict[str, Any]:
37
+ results, warnings = search_memories(self.db, query, workspace or self.settings.workspace, tags, limit)
38
+ return self.db.state.response({"results": results, "count": len(results)}, extra_warnings=warnings)
39
+
40
+ def memory_compare(self, left_id: Optional[int] = None, right_id: Optional[int] = None, left: Optional[dict[str, Any]] = None, right: Optional[dict[str, Any]] = None, **_: Any) -> dict[str, Any]:
41
+ left_record = left or (self.db.get_memory(int(left_id)) if left_id is not None else None)
42
+ right_record = right or (self.db.get_memory(int(right_id)) if right_id is not None else None)
43
+ if not left_record or not right_record:
44
+ return self.db.state.response({"error": "left and right records are required"}, ok=False)
45
+ return self.db.state.response({"comparison": compare_memories(left_record, right_record), "left": left_record, "right": right_record})
46
+
47
+ def memory_arbitrate(self, left_id: int, right_id: int, mark_conflict: bool = True, apply: bool = False, **_: Any) -> dict[str, Any]:
48
+ left = self.db.get_memory(int(left_id))
49
+ right = self.db.get_memory(int(right_id))
50
+ if not left or not right:
51
+ return self.db.state.response({"error": "memory id not found"}, ok=False)
52
+ comparison = compare_memories(left, right)
53
+ conflict_id = None
54
+ if mark_conflict:
55
+ reason = "; ".join(comparison["reasons"])
56
+ conflict_id = self.db.record_conflict(int(left_id), int(right_id), left.get("subject") or right.get("subject"), reason, comparison["winner_id"])
57
+ applied = False
58
+ if apply and comparison["winner_id"] and comparison["loser_id"] and not comparison["manual_review"]:
59
+ loser = self.db.get_memory(int(comparison["loser_id"]))
60
+ if loser and loser.get("protection_level") != ProtectionLevel.LOCKED.value and loser.get("source_type") != SourceType.USER_CONFIRMED.value:
61
+ applied = self.db.update_memory(int(comparison["loser_id"]), {"status": "superseded"})
62
+ return self.db.state.response({"comparison": comparison, "conflict_id": conflict_id, "applied": applied})
63
+
64
+ def memory_list_conflicts(self, status: str = "open", limit: int = 50, **_: Any) -> dict[str, Any]:
65
+ conflicts = self.db.list_conflicts(status=status, limit=int(limit))
66
+ return self.db.state.response({"conflicts": conflicts, "count": len(conflicts)})
67
+
68
+ def memory_confirm(self, memory_id: int, source_ref: Optional[str] = None, confidence: float = 1.0, **_: Any) -> dict[str, Any]:
69
+ memory = self.db.get_memory(int(memory_id))
70
+ if not memory:
71
+ return self.db.state.response({"error": "memory id not found"}, ok=False)
72
+ metadata = dict(memory.get("metadata") or {})
73
+ metadata["confirmed_from"] = source_ref or "manual"
74
+ ok = self.db.update_memory(
75
+ int(memory_id),
76
+ {
77
+ "source_type": SourceType.USER_CONFIRMED.value,
78
+ "confidence": float(confidence),
79
+ "protection_level": ProtectionLevel.LOCKED.value,
80
+ "status": "active",
81
+ "metadata": metadata,
82
+ },
83
+ )
84
+ updated = self.db.get_memory(int(memory_id)) if ok else memory
85
+ return self.db.state.response({"confirmed": ok, "record": updated})
86
+
87
+ def memory_status(self, **_: Any) -> dict[str, Any]:
88
+ return self.db.state.response(
89
+ {
90
+ "db_path": str(self.settings.db_path),
91
+ "backup_jsonl": str(self.settings.backup_jsonl),
92
+ "sqlite_vec_available": self.db.state.sqlite_vec_available,
93
+ "fts5_available": self.db.state.fts5_available,
94
+ "sqlite_writable": self.db.state.sqlite_writable,
95
+ "jsonl_backup_active": self.db.state.jsonl_backup_active,
96
+ "client": self.settings.client,
97
+ "agent_id": self.settings.agent_id,
98
+ "workspace": self.settings.workspace,
99
+ "policy": {
100
+ "client_defaults": self.settings.policy.client_defaults,
101
+ "default_enabled": self.settings.policy.default_enabled,
102
+ "allow_agents": self.settings.policy.allow_agents,
103
+ "deny_agents": self.settings.policy.deny_agents,
104
+ },
105
+ }
106
+ )
@@ -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,15 @@
1
+ memory_arbiter/__init__.py,sha256=MK_3xJdjTbJvb7M4a2awe38mp6Q5pLaO9OxCWjbJSCc,57
2
+ memory_arbiter/arbitration.py,sha256=q5kSm5TPenazamzV-rFnZXTTdQbeGhg-lo4HeUF9aNw,4144
3
+ memory_arbiter/config.py,sha256=EitfTJijPPWP5NjVvk9vFk_iH2EEl3iTE9y3-K6Fs0U,2715
4
+ memory_arbiter/db.py,sha256=ilsWjiGVoq39V4VdHAVsTds8Q7gN_9Mw6Wmsou6y5bo,10185
5
+ memory_arbiter/degrade.py,sha256=vLk_gmSMh6wOb56e35kY4pW3SDoJJ2iqtrFwD4x-hhU,1091
6
+ memory_arbiter/models.py,sha256=EIeMRTDRb7qe63GqKeETyuXe0AJp8giv0UXjj-ZVwoo,3048
7
+ memory_arbiter/search.py,sha256=DWFr2a_UmcmobbNRlxq5BXGkCJiTZPhB8usFD5wdHLo,2119
8
+ memory_arbiter/server.py,sha256=VRKfaJ0e39iWgxaLx7ctybeLRq9KQZFYahk7PYSMCGM,3946
9
+ memory_arbiter/tools.py,sha256=xsGGgIAcqcAVYosR05zyiYHrxFgvaTwavnrOYCmAAMs,6233
10
+ memory_arbiter_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=26R1Y1S9Zz9xhwCqKr1BOiXRjk-Z8bSK9lC8wSVaPDk,1067
11
+ memory_arbiter_mcp-0.1.0.dist-info/METADATA,sha256=oA6f70bn16Nlt1ur8_wsvhXOlbJRiVQe0F8aTpSt4ts,7776
12
+ memory_arbiter_mcp-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ memory_arbiter_mcp-0.1.0.dist-info/entry_points.txt,sha256=Wkl1Xqd0vLB4Rb3c8sc6Gsgk14TfVmBx4x7pBonPaoI,66
14
+ memory_arbiter_mcp-0.1.0.dist-info/top_level.txt,sha256=FTEDicSQQ34Y267bTtOdFbxwBOSU4jL_ihE4mF5hMBI,15
15
+ memory_arbiter_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ memory-arbiter-mcp = memory_arbiter.server:main
@@ -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 @@
1
+ memory_arbiter