Branching & Isolation
HatiData V2 replaces V1's implicit branch fallback with explicit visibility modes — giving agents and reviewers precise control over what data they see.
Why Visibility Modes?
In V1, branch queries used a single rule: "read branch first, fall back to mainline." This caused subtle bugs:
- Verification agents saw branch-scoped drafts instead of canonical mainline data
- Merge conflicts were invisible until the merge attempt
- Contract-driven agents couldn't guarantee they were reading the exact key they needed
V2 makes visibility explicit. Every memory read specifies a mode.
Visibility Mode Matrix
| Mode | Reads From | Falls Back? | Use Case |
|---|---|---|---|
MainOnly | Mainline (branch_id IS NULL) | No | Verification agents, release decisions — need canonical truth |
BranchLocal | Current branch only | No | Isolated experimentation, what-if scenarios |
BranchWithFallback | Branch first, then mainline | Yes | Normal agent work on a branch — sees own changes + shared context |
ExactKey | Exact key match, no search | No | Contract-driven agents — "give me proj:abc:api_contract or fail" |
SDK Usage
from hatidata import VisibilityMode
# Verification agent: must see canonical state
results = client.search_memory(
query="architecture decisions",
visibility=VisibilityMode.MAIN_ONLY,
)
# Branch agent: see own work + shared context
results = client.search_memory(
query="build log",
branch_id="branch-feature-x",
visibility=VisibilityMode.BRANCH_WITH_FALLBACK,
)
# Contract agent: exact key or error
result = client.load_memory_exact(
key="proj:abc:api_contract",
visibility=VisibilityMode.EXACT_KEY,
)
SQL Syntax
-- MainOnly: explicit filter
SELECT * FROM agent_memories
WHERE project_id = 'proj-abc'
AND branch_id IS NULL;
-- BranchWithFallback: COALESCE pattern
SELECT * FROM agent_memories
WHERE project_id = 'proj-abc'
AND (branch_id = 'branch-feature-x' OR branch_id IS NULL)
ORDER BY branch_id NULLS LAST -- branch wins over mainline
LIMIT 1;
The "No-Leak" Guarantee
Branch-scoped data never appears in mainline queries. This is enforced at three layers:
Layer 1: Query Rewriting
The HatiData proxy rewrites queries based on visibility mode. A MainOnly query physically cannot return branch rows — the WHERE branch_id IS NULL clause is injected before execution.
Layer 2: ABAC Enforcement
Branch isolation is backed by ABAC policies. An agent with MainOnly access that attempts to read a branch-scoped key gets an explicit AccessDenied error, not empty results.
Layer 3: Merge Provenance
When branch data is merged to mainline, new rows are created — branch rows are never modified. This means:
- The original branch data is preserved for audit
- Merge creates a clear before/after trail
- Rollback = delete the merged mainline rows (branch originals intact)
Creating and Managing Branches
Create a Branch
branch = client.create_branch(
project_id="proj-abc",
name="feature-auth-refactor",
parent_branch_id=None, # Fork from mainline
)
# branch.id = "branch-abc-123"
Write to a Branch
Memory writes automatically scope to the active branch:
client.store_memory(
key="proj:abc:api_contract",
value={"openapi": "3.1.0", "paths": {...}},
branch_id="branch-abc-123",
)
# Creates a branch-scoped row; mainline row unchanged
Inspect Branch Divergence
Before merging, review what the branch changed:
SELECT
memory_key,
'branch' AS source,
updated_at
FROM agent_memories
WHERE branch_id = 'branch-abc-123'
EXCEPT
SELECT
memory_key,
'mainline' AS source,
updated_at
FROM agent_memories
WHERE branch_id IS NULL
AND project_id = 'proj-abc';
Or use the convenience view:
SELECT * FROM v_branch_divergence
WHERE branch_id = 'branch-abc-123'
ORDER BY modified_at DESC;
Merge a Branch
merge_result = client.merge_branch(
branch_id="branch-abc-123",
strategy="last_writer_wins", # or "manual_review"
)
# merge_result.merged_keys = ["proj:abc:api_contract", "proj:abc:db_schema"]
# merge_result.conflicts = []
Merge strategies:
| Strategy | Behavior | Use Case |
|---|---|---|
last_writer_wins | Branch value overwrites mainline | Most agent workflows |
manual_review | Creates ReviewRequest for each conflict | Critical artifacts |
Branch Lifecycle with Tasks
When a task runs on a branch, all its artifacts are branch-scoped:
Task created (branch: feature-x)
└── Attempt runs
├── Reads: BranchWithFallback (sees branch + mainline)
├── Writes: Branch-scoped (only to feature-x)
└── Artifacts: branch-scoped instances
After the branch is merged, the artifacts become mainline-visible — but the original branch-scoped rows remain for audit.
Security Considerations
- Branch deletion does not delete memory rows — rows are tombstoned for audit retention
- Cross-branch reads are forbidden by default; require explicit ABAC policy grant
- Nested branches (branch of a branch) are supported but limited to 3 levels deep
- Concurrent merges to the same mainline key use optimistic locking — second merge gets a conflict error
Next Steps
- Branch Isolation (V1) — Original branching concepts and copy-on-write implementation
- Tasks & Attempts — How branches affect task execution
- Security Model — ABAC policies for branch access control