Multiple AI agents (Claude Desktop, Claude Code, future agents) share write access to a Graphiti knowledge graph without coordinated protocols. This has resulted in:
- Namespace pollution: 97 episodes in
graphiti_meta_knowledgevs intended 8-10. - Duplicate episodes: Lack of idempotency checks.
- Inconsistent content categorization: Different agents, different rules.
- Loss of namespace semantic integrity.
| Criterion | Single Writer Orchestrator | Uniform MCP Guardrails | Event Sourcing + Write-Ahead Log | Namespace Ownership Model |
|---|---|---|---|---|
| Coordination Complexity | Low (single point of control) | Medium (distributed enforcement) | High (log reconciliation) | Medium (ownership registry) |
| Implementation Effort | Medium (new orchestrator service) | Low (extend existing MCP) | High (new infrastructure) | Low (config + conventions) |
| Latency Impact | +50-100ms (hop through orchestrator) | +10ms (validation checks) | +20-50ms (log writes) | None (static routing) |
| Fault Tolerance | Low (single point of failure) | High (distributed) | Very High (replay capability) | Medium (degraded if registry unavailable) |
| Multi-Agent Scalability | Limited (bottleneck) | High (horizontal) | Very High (append-only) | High (partitioned) |
| Existing Agent Compatibility | Low (requires refactor) | High (transparent injection) | Low (API changes) | Medium (config changes) |
| Auditability | High (central logs) | Medium (distributed logs) | Very High (complete history) | Medium (ownership records) |
| Recovery from Corruption | Manual cleanup | Manual cleanup | Replay from checkpoint | Scoped to namespace |
Hybrid approach: Namespace Ownership Model + Uniform MCP Guardrails
- Short-term: Define namespace ownership and inject guardrails via MCP.
- Medium-term: Build orchestrator for high-coordination namespaces.
- Long-term: Event sourcing for audit and recovery requirements.
ADR-001: Multi-Agent Graph Write Coordination
- Status: Proposed
Context: The Graphiti knowledge graph serves as shared long-term memory for multiple AI agents. Without coordination:
- Claude Desktop wrote 89 episodes to
graphiti_meta_knowledgewithout namespace-appropriate filtering. - Claude Code, after
/verifyguardrails, shows dramatically reduced pollution. - No mechanism exists to enforce "Robert's Rules" across heterogeneous agents.
Decision: Implement a Namespace Registry + MCP Guardrail Injection pattern:
- Namespace Registry (
docs/NAMESPACE_REGISTRY.yaml):
- Each namespace has: purpose, allowed_content_types, size_limit, owner_agent.
- Owner has write access; others read-only or require explicit delegation.
- Guardrail MCP Server (for Claude Desktop and other agents):
- Wraps
graphiti-localMCP. - Injects
/verifyequivalent before allowing writes. - Validates namespace appropriateness before
add_memory.
- Write Delegation Protocol:
- Non-owner agents submit write requests to a queue.
- Owner agent (or human) approves/routes to correct namespace.
Alternatives Considered:
| Alternative | Rejected Because |
|---|---|
| Single centralized orchestrator | Single point of failure; latency; requires all agents to refactor. |
| Optimistic locking per episode | Graphiti doesn't support CAS operations; race conditions remain. |
| Full event sourcing | Over-engineered for current scale; high implementation cost. |
| Honor system (documentation only) | Already failed; Claude Desktop ignored conventions. |
Consequences:
-
Positive:
- Backward compatible (existing agents continue working).
- Namespace integrity enforced at protocol level.
- Clear ownership enables accountability.
- MCP guardrails reusable across agent types.
-
Negative:
- Requires maintaining namespace registry.
- MCP wrapper adds ~10ms latency per write.
- Cross-namespace queries unaffected (coordination is write-side only).
-
Risks:
- Registry drift if not kept synchronized.
- Performance degradation under high write volume.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AGENT LAYER β
βββββββββββββββββββββββ¬ββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ€
β Claude Code β Claude Desktop β Future Agents β
β βββββββββββββββ β βββββββββββββββ β βββββββββββββββ β
β β /verify β β β β β β β β
β β /health β β β (no native β β β (unknown β β
β β /graph-statsβ β β guardrails)β β β capabilities) β
β ββββββββ¬βββββββ β ββββββββ¬βββββββ β ββββββββ¬βββββββ β
ββββββββββββΌβββββββββββ΄βββββββββββΌβββββββββββ΄βββββββββββΌβββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β COORDINATION LAYER β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Guardrail MCP Server β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β Namespace β β Pre-Write β β Write β β β
β β β Registry β β Validation β β Delegation β β β
β β β Lookup β β (purpose, β β Queue β β β
β β β β β size, type) β β β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββ β
β β Namespace Registry β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β graphiti_meta_knowledge: β β β
β β β purpose: "Getting started guide for new Claude instances" β β β
β β β owner: human_curated β β β
β β β allowed_types: [Getting Started, Core Lesson] β β β
β β β size_limit: 15 β β β
β β β write_policy: explicit_approval_required β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β
β β β technical_patterns: β β β
β β β purpose: "Architecture patterns and technical learnings" β β β
β β β owner: any_agent β β β
β β β allowed_types: [Pattern, Lesson, Analysis, Anti-Pattern] β β β
β β β size_limit: 100 β β β
β β β write_policy: self_service β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β STORAGE LAYER β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β graphiti-local MCP β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β add_memory β β search_nodes β β get_episodes β β β
β β β β β β β β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Neo4j Aura (Graph Storage) β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β :Episodic nodes β :Entity nodes β :RELATES_TO edges β β β
β β β (group_id indexed) β (embeddings) β (temporal validity) β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# guardrail_mcp/server.py
from fastmcp import FastMCP
from pydantic import BaseModel
import yaml
mcp = FastMCP("graphiti-guardrail")
# Load namespace registry
with open("docs/NAMESPACE_REGISTRY.yaml") as f:
NAMESPACE_REGISTRY = yaml.safe_load(f)
class WriteRequest(BaseModel):
name: str
episode_body: str
group_id: str
source: str = "text"
source_description: str = ""
@mcp.tool()
async def guarded_add_memory(request: WriteRequest) -> dict:
"""Add memory with namespace validation and guardrails."""
# 1. Lookup namespace config
ns_config = NAMESPACE_REGISTRY.get(request.group_id)
if ns_config is None:
return {
"status": "error",
"message": f"Unknown namespace: {request.group_id}. "
f"Available: {list(NAMESPACE_REGISTRY.keys())}"
}
# 2. Check write policy
if ns_config["write_policy"] == "explicit_approval_required":
return {
"status": "blocked",
"message": f"Namespace '{request.group_id}' requires explicit approval. "
f"Purpose: {ns_config['purpose']}. "
f"Consider: {ns_config.get('suggested_alternative', 'technical_patterns')}"
}
# 3. Validate content type (heuristic based on episode name prefix)
allowed_types = ns_config.get("allowed_types", [])
episode_type = request.name.split(":")[0] if ":" in request.name else "Unknown"
if allowed_types and episode_type not in allowed_types:
return {
"status": "warning",
"message": f"Episode type '{episode_type}' not in allowed types {allowed_types}. "
f"Proceeding with advisory.",
"proceed": True
}
# 4. Check size limit
current_count = await get_namespace_episode_count(request.group_id)
if current_count >= ns_config.get("size_limit", float("inf")):
return {
"status": "blocked",
"message": f"Namespace '{request.group_id}' at capacity ({current_count}/{ns_config['size_limit']}). "
f"Consider cleanup or alternative namespace."
}
# 5. Delegate to underlying graphiti-local
result = await graphiti_add_memory(
name=request.name,
episode_body=request.episode_body,
group_id=request.group_id,
source=request.source,
source_description=request.source_description
)
return {"status": "success", "result": result}# docs/NAMESPACE_REGISTRY.yaml
namespaces:
graphiti_meta_knowledge:
purpose: "Lightweight getting started guide for new Claude instances"
owner: human_curated
allowed_types:
- "Getting Started"
- "Core Lesson"
- "Procedure"
size_limit: 15
write_policy: explicit_approval_required
suggested_alternative: technical_patterns
graphiti_reference_docs:
purpose: "Official Graphiti documentation and API reference"
owner: human_curated
allowed_types:
- "Documentation"
- "Code Example"
- "API Reference"
size_limit: 50
write_policy: explicit_approval_required
technical_patterns:
purpose: "Architecture patterns, lessons, and technical learnings"
owner: any_agent
allowed_types:
- "Pattern"
- "Lesson"
- "Analysis"
- "Anti-Pattern"
- "Best Practice"
size_limit: 100
write_policy: self_service
session_reflections:
purpose: "Cross-session learnings and agent reflections"
owner: any_agent
allowed_types:
- "Reflection"
- "Lesson"
- "Insight"
size_limit: 200
write_policy: self_service
# Dynamic project namespaces follow pattern:
# {project_name}_{domain}:
# purpose: "Project-specific knowledge"
# owner: project_agent
# write_policy: self_service
For complex multi-agent workflows, integrate with LangGraph supervisor:
from langgraph_supervisor import create_supervisor
from langgraph.prebuilt import create_react_agent
# Reader agents - can query graph, cannot write
reader_agent = create_react_agent(
model=model,
tools=[search_nodes, search_memory_facts, get_episodes],
name="graph_reader",
prompt="You can search and retrieve from the knowledge graph. Do not attempt writes."
)
# Writer agent - single writer with validation
writer_agent = create_react_agent(
model=model,
tools=[guarded_add_memory], # Uses guardrail MCP
name="graph_writer",
prompt=(
"You manage writes to the knowledge graph. "
"Always validate namespace appropriateness before writing. "
"Route to correct namespace based on content type."
)
)
# Supervisor orchestrates access
graph_supervisor = create_supervisor(
agents=[reader_agent, writer_agent],
model=model,
prompt=(
"You coordinate knowledge graph access. "
"For queries, use graph_reader. "
"For writes, use graph_writer who will validate namespace."
)
)β DON'T: Create separate namespaces for each agent instance
claude_desktop_memories/
claude_code_session_123/
claude_code_session_456/
β DO: Use semantic namespaces based on content domain
technical_patterns/
project_architectures/
career_opportunities/
Why: Agent-based namespacing defeats knowledge sharing and creates silos.
# β DON'T: Let agents guess namespace
await add_memory(
name="Important Lesson",
episode_body="...",
group_id="graphiti_meta_knowledge" # Agent guessed this
)
# β
DO: Explicit namespace validation with guardrails
result = await guarded_add_memory(
name="Lesson: Important Discovery",
episode_body="...",
group_id="technical_patterns" # Validated against registry
)
if result["status"] == "blocked":
# Handle redirectionβ DON'T: "Just put it in meta_knowledge, we'll organize later"
- Result: 97 episodes, most misplaced
β DO: Validate before write, route to correct namespace
| Scenario | Recommendation |
|---|---|
| Single agent, single user | Skip coordination layer; direct access sufficient |
| High-frequency writes (>100/min) | Consider write-ahead log + batch processing |
| Strict consistency requirements | Use database transactions, not MCP coordination |
| Cross-namespace queries dominant | Focus on read optimization, not write coordination |
async def validate_before_write(
episode_name: str,
target_namespace: str,
registry: dict
) -> tuple[bool, str, str | None]:
"""
Returns: (is_valid, message, suggested_namespace)
"""
ns_config = registry.get(target_namespace)
if not ns_config:
# Unknown namespace - suggest closest match
suggested = find_closest_namespace(target_namespace, registry)
return (False, f"Unknown namespace", suggested)
# Extract episode type from naming convention "Type: Topic - Aspect"
episode_type = episode_name.split(":")[0].strip() if ":" in episode_name else None
if not episode_type:
return (False, "Episode name must follow 'Type: Topic - Aspect' convention", None)
allowed = ns_config.get("allowed_types", [])
if allowed and episode_type not in allowed:
# Find namespace that accepts this type
suggested = find_namespace_for_type(episode_type, registry)
return (False, f"Type '{episode_type}' not allowed in '{target_namespace}'", suggested)
# Check capacity
current_count = await get_episode_count(target_namespace)
limit = ns_config.get("size_limit", float("inf"))
if current_count >= limit:
return (False, f"Namespace at capacity ({current_count}/{limit})", None)
return (True, "Validation passed", None)# .claude/commands/write-episode.md
"""
Before writing to the knowledge graph, execute the observe-before-act protocol.
## Step 1: Verify Environment
```python
status = mcp__graphiti-local__get_status()
if status["status"] != "ok":
STOP - Environment not ready
episodes = mcp__neo4j-cypher__read_neo4j_cypher(
query='''
MATCH (e:Episodic)
WHERE e.group_id = $namespace
RETURN count(*) as count,
collect(e.name)[0..5] as recent_episodes
''',
params={"namespace": TARGET_NAMESPACE}
)
# Review: Does this content belong with existing episodes?- Episode name must match: "Type: Topic - Aspect"
- Allowed types for namespace: {from registry}
- If mismatch, suggest alternative namespace
mcp__graphiti-local__add_memory(
name=EPISODE_NAME,
episode_body=CONTENT,
group_id=TARGET_NAMESPACE,
source_description=SOURCE
)- Wait 15-20 seconds for async processing
- Confirm episode appears in get_episodes
"""
class NamespaceOwnership:
"""Enforces single-writer pattern at namespace level."""
OWNERSHIP_MAP = {
"graphiti_meta_knowledge": "human",
"graphiti_reference_docs": "human",
"technical_patterns": "any",
"session_reflections": "any",
# Project namespaces owned by creating agent
}
@classmethod
def can_write(cls, agent_id: str, namespace: str) -> bool:
owner = cls.OWNERSHIP_MAP.get(namespace, "any")
if owner == "any":
return True
if owner == "human":
return agent_id == "human_operator"
if owner == agent_id:
return True
return False
@classmethod
def delegate_write(cls, from_agent: str, to_agent: str, namespace: str) -> bool:
"""Explicit delegation of write permission."""
# Implementation: Add to delegation table with TTL
pass| Component | Implementation | Priority |
|---|---|---|
| Namespace Registry | docs/NAMESPACE_REGISTRY.yaml |
P0 - Immediate |
| Agent Protocols | docs/AGENT_GRAPH_PROTOCOLS.md |
P0 - Immediate |
| Guardrail MCP | guardrail_mcp/server.py |
P1 - Short-term |
| Namespace Cleanup | AGE-38 through AGE-45 | P1 - Short-term |
| LangGraph Supervisor Integration | Optional pattern | P2 - Medium-term |
| Event Sourcing + Replay | Future enhancement | P3 - Long-term |
Key Insight: The
/verify,/health,/graph-statscommands already solved the problem for Claude Code by establishing observe-before-act as protocol. The remaining work is extending this pattern to all agents via MCP guardrails and formalizing namespace ownership.