Skip to main content

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

ModeReads FromFalls Back?Use Case
MainOnlyMainline (branch_id IS NULL)NoVerification agents, release decisions — need canonical truth
BranchLocalCurrent branch onlyNoIsolated experimentation, what-if scenarios
BranchWithFallbackBranch first, then mainlineYesNormal agent work on a branch — sees own changes + shared context
ExactKeyExact key match, no searchNoContract-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:

StrategyBehaviorUse Case
last_writer_winsBranch value overwrites mainlineMost agent workflows
manual_reviewCreates ReviewRequest for each conflictCritical 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

Artifact Promotion (Branch → Mainline)

At phase boundaries, branched artifacts need to be promoted to mainline so downstream phases can find them without branch-awareness.

# Promote a branched artifact to mainline (sets branch_id = null)
curl -X POST "https://api.hatidata.com/v2/runtime/artifacts/${ARTIFACT_ID}/promote" \
-H "Authorization: ApiKey hd_live_..."

Key properties:

  • In-place update: artifact ID does not change (preserves lineage edges)
  • Idempotent: promoting an already-mainline artifact is a no-op
  • Preserves: validation_status, confidence, content_hash, artifact_key

Typical merge workflow at a phase boundary:

# 1. Merge memories/reasoning (V1 wire protocol)
hatidata.branch_merge(branch_id)

# 2. Promote V2 artifacts from each branch to mainline
artifacts = v2_client.list_artifacts(project_id=project_id)
for art in artifacts:
if art.branch_id is not None:
v2_client.promote_artifact(art.id)

# 3. Advance to next phase — downstream sees mainline artifacts
advance_to_phase(next_phase)

See V2 Runtime API Reference for full details.

Stay in the loop

Product updates, engineering deep-dives, and agent-native insights. No spam.