The MentisDB Agent Memory Cookbook

Patterns and recipes for building AI agents that remember

2.4 Federated Team Memory

The problem

Two teams share a code-review agent. The backend team's chain is full of database migrations, API contracts, and rate-limit decisions. The frontend team's chain is full of design tokens, accessibility lints, and component-library rules. Each team has its own retrieval quality bar and its own appetite for noise.

Merge both teams into one giant chain and after a quarter, frontend queries surface backend constraints as false positives and either team's mistakes poison retrieval for the other. Keep them completely isolated and cross-team architectural decisions ("we standardized on OpenAPI 3.1") get re-debated and re-written twice.

The fix is federated team memory: each team or project gets its own chain, a shared root chain holds cross-cutting decisions, and a single query transparently searches across all of them — ranking the results into one list while preserving the boundary.

Why it's hard

The pattern

MentisDB gives you three primitives. Compose them to taste:

  1. mentisdb_branch_from() — create a child chain that diverges from a specific thought on a parent. The branch's genesis thought is a StateSnapshot with a BranchesFrom relation pointing back to the branch-point. The parent is not modified.
  2. mentisdb_federated_search() — search any explicit list of chains at once. Results come back as a single ranked list, each hit annotated with its chain_key.
  3. mentisdb_merge_chains() — when a branch is done (a release shipped, an experiment concluded), copy every source thought into a target chain with autonomous agent remapping, then permanently delete the source.

There's a quieter fourth primitive: any single-chain ranked search on a branch transparently includes its ancestor chains. Walking the BranchesFrom relations is automatic. The branch behaves as if the parent's memories were locally available, but the response still marks every hit with its origin chain.

Mental model: a chain is a stream of immutable thoughts. A branch is a new stream that knows where it came from. A federated query is a fan-out read. A merge is an irreversible write. Reads federate; writes never silently cross boundaries.

Implementation

1. Naming the chains

Pick a hierarchy and commit to it. The single most common cause of federated-memory pain in production is unprincipled chain naming. Two patterns that survive scale:

The / in a chain key is just a character — MentisDB doesn't interpret it — but using it as a visual delimiter makes dashboard views, MCP responses, and federation config easier to scan.

2. Stand up the root and branch with Rust

The example below builds an org-shared chain, branches team-backend from one of its decisions, and stacks a project branch on top of that. Federated search and the merge come next.

use mentisdb::{
    MentisDb, ThoughtInput, ThoughtType, ThoughtRole,
    RankedSearchQuery, RankedSearchFilter,
};
use std::path::Path;
use std::io;

fn main() -> io::Result<()> {
    let dir = tempfile::tempdir()?;
    let chain_dir = dir.path();

    // 1. Root chain: cross-cutting org-wide decisions.
    let mut root = MentisDb::open_with_key(chain_dir, "org-shared")?;
    root.upsert_agent("architect", Some("Chief Architect"),
        Some("org"), Some("Writes org-wide constraints"), None)?;

    let api_decision = root.append_thought("architect",
        ThoughtInput::new(ThoughtType::Decision,
            "All public HTTP APIs use OpenAPI 3.1; \
             contract-first, no hand-rolled clients.")
            .with_concepts(["api", "openapi", "contracts"])
            .with_tags(["scope:org", "policy"])
            .with_importance(0.9)
            .with_role(ThoughtRole::Memory)
    )?.clone();

    root.append_thought("architect",
        ThoughtInput::new(ThoughtType::Constraint,
            "Rate-limit headers must use the IETF RateLimit fields.")
            .with_concepts(["rate-limit", "http"])
            .with_tags(["scope:org", "policy"])
            .with_importance(0.85)
    )?;

    // 2. Branch the backend team off the API decision.
    //    The branch's genesis StateSnapshot carries a
    //    BranchesFrom relation pointing at `api_decision.id`.
    let mut backend = MentisDb::branch_from(
        chain_dir, "org-shared", api_decision.id, "team-backend",
    )?;
    backend.upsert_agent("backend-bot", Some("Backend Reviewer"),
        Some("team-backend"), None, None)?;

    backend.append_thought("backend-bot",
        ThoughtInput::new(ThoughtType::Decision,
            "Backend uses axum 0.7 + sqlx for new services.")
            .with_concepts(["stack", "rust", "axum", "sqlx"])
            .with_tags(["scope:team", "team-backend"])
            .with_importance(0.8)
    )?;

    // 3. A branch of a branch is still a branch — the ancestor
    //    walk finds both `team-backend` AND `org-shared`.
    let last_backend = backend.thoughts().last().unwrap().id;
    let mut auth = MentisDb::branch_from(
        chain_dir, "team-backend", last_backend,
        "team-backend/auth-rewrite-2026q3",
    )?;
    auth.upsert_agent("backend-bot", None,
        Some("team-backend"), None, None)?;
    auth.append_thought("backend-bot",
        ThoughtInput::new(ThoughtType::Plan,
            "Replace bespoke JWT validation with `jsonwebtoken`; \
             migrate in 4 steps across 3 services.")
            .with_concepts(["auth", "jwt", "migration"])
            .with_tags(["scope:project", "auth-rewrite"])
            .with_importance(0.75)
    )?;

    Ok(())
}

The directory layout on disk is now three independent append-only chains. They share no thoughts. The only link is the BranchesFrom relation in each branch's genesis thought — a single typed edge the server uses to walk the ancestor list at query time.

3. Federated search across the whole tree

A code-review agent wants to know "what do we say about API contracts?" and doesn't care which team wrote the rule. Use mentisdb_federated_search with an explicit chain list:

{
  "tool": "mentisdb_federated_search",
  "arguments": {
    "chain_keys": [
      "org-shared",
      "team-backend",
      "team-backend/auth-rewrite-2026q3"
    ],
    "text": "API contracts and rate limits",
    "thought_types": ["Decision", "Constraint", "PreferenceUpdate"],
    "limit": 10,
    "enable_reranking": true
  }
}

The server runs the same ranked query against each chain, then merges the per-chain result sets with Reciprocal Rank Fusion. Every hit carries its chain_key so the agent can render provenance:

=== Federated results (3 chains, top 4) ===
[org-shared          ] Decision  : All public HTTP APIs use OpenAPI 3.1...
[org-shared          ] Constraint: Rate-limit headers must use IETF...
[team-backend        ] Decision  : Backend uses axum 0.7 + sqlx...
[team-backend        ] PreferenceUpdate: Prefer `tracing` macros...

Critically, the same agent on a single-chain query against team-backend/auth-rewrite-2026q3 will also see the org-shared and team-backend hits — because branch search transparently walks ancestors. The federated call is for when you want to fan out across sibling chains (multiple teams, multiple tenants you administer); the ancestor walk happens automatically on every branch query.

Two reads, same model: "search this branch" → automatic ancestor walk. "search these N chains" → explicit federated query. Both return one ranked list with chain_key stamped on every hit. Your agent code never has to merge by hand.

4. Merging a finished branch back

The auth-rewrite-2026q3 project shipped. Its lessons should belong to team-backend permanently; the branch as a separate chain is now noise.

// MCP call
{
  "tool": "mentisdb_merge_chains",
  "arguments": {
    "source_chain_key": "team-backend/auth-rewrite-2026q3",
    "target_chain_key": "team-backend"
  }
}

// Response
{ "thoughts_copied": 47, "agents_remapped": 1, "source_deleted": true }

merge_chains is autonomous about agent identity: each source agent is matched to the closest existing target agent by Jaccard character-set similarity, with thought-count as the tiebreaker. No new agents are created on the target chain. Register any new agents on the target before merging or their thoughts will be relabeled. Cross-chain refs indices are also dropped — replace them with UUID-based relations beforehand if you need them to survive.

merge_chains is destructive. The source is deleted. Back up the directory before merging anything you might want to inspect later, and run the merge from one process at a time.

Production notes

When to branch vs. write to a shared chain

Branch when the lifetime of the work is bounded. If team-backend is the team's permanent brain, write into it directly. If auth-rewrite-2026q3 is a finite project, branch it, do the work, merge (or archive) when done. Branches are cheap to create and cheap to delete; they exist precisely so a polluted experiment doesn't poison a durable chain.

Naming conventions: pick one in week one

Renaming a chain at scale is a migration: every webhook, saved query, bookmark, and opencode.json entry has to change in lockstep. Set a convention before you have ten chains:

The "no orphans" rule

Every long-lived chain should be reachable from some root. If a branch's parent is deleted, the ancestor-walk silently terminates and you've lost transparent access to the root's decisions. Never delete a chain with live descendants, and audit descendants before any merge_chains that deletes their source ancestor.

Multi-tenancy and the security boundary

Federated search is a capability, not a permission system. Anyone holding a list of chain keys can fan a query across them. For multi-tenant deployments:

The leakiest mistake is branching a tenant's chain from a chain that another tenant also branches from, while writing tenant-specific data into that shared ancestor. Keep ancestors strictly policy-only: facts that every descendant is allowed to see.

Federated search is not free

Each chain in chain_keys is a real query — cost scales linearly. For a federation of 50 chains, either narrow with strong filters (tags, types, time windows) or pre-aggregate via merge_chains into a smaller set of rollup chains updated on a schedule.

Querying patterns

"What does any team say about JWT?"

mentisdb_federated_search({
  chain_keys: list_team_chains(),
  text: "JWT validation",
  concepts_any: ["auth", "jwt"],
  thought_types: ["Decision", "Constraint", "LessonLearned"],
  limit: 20,
  enable_reranking: true,
})

"On this branch, including everything the parent decided"

// Single-chain query. Ancestors are walked automatically.
mentisdb_ranked_search({
  chain_key: "team-backend/auth-rewrite-2026q3",
  text: "rate limiting",
  limit: 10,
})
// Hits carry chain_key annotations for whichever ancestor
// (or the branch itself) actually owns each thought.

Testing this pattern

The most important regression to catch is that the ancestor walk still surfaces a parent's decision from a branch query:

#[test]
fn branch_walks_ancestor_on_search() {
    let dir = tempfile::tempdir().unwrap();
    let mut parent = MentisDb::open_with_key(dir.path(), "parent")
        .unwrap();
    let pivot = parent.append("a",
        ThoughtType::Decision,
        "Use OpenAPI 3.1.").unwrap().clone();

    let _branch = MentisDb::branch_from(
        dir.path(), "parent", pivot.id, "branch",
    ).unwrap();

    // Single-chain search on the branch surfaces the parent's
    // decision via the ancestor walk, with chain_key annotated.
    let res = run_ranked_search("branch", "OpenAPI");
    assert!(res.hits.iter().any(|h|
        h.chain_key == "parent" &&
        h.thought.content.contains("OpenAPI")
    ));
}

What's next

Federation gives multiple agents one queryable memory without forcing them into one chain. The next pattern, Webhook-Driven Workflows, shows how to make those chains react — firing HTTP callbacks the moment a thought is appended, so downstream agents and pipelines can wake up on the new memory instead of polling.