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
- Chains are append-only. You can't retroactively split a polluted shared chain.
- Cross-chain references break: thought indices are chain-local, so naive merging produces dangling refs.
- Multi-tenancy needs a security boundary: tenant A must never see tenant B's data, even on a federated query.
- Branches that fork from a parent need a way to "rejoin" without losing the parent's history.
- Naming chains badly traps you: once
team-foo-2026has 50,000 thoughts, renaming is an operational event.
The pattern
MentisDB gives you three primitives. Compose them to taste:
-
mentisdb_branch_from()— create a child chain that diverges from a specific thought on a parent. The branch's genesis thought is aStateSnapshotwith aBranchesFromrelation pointing back to the branch-point. The parent is not modified. -
mentisdb_federated_search()— search any explicit list of chains at once. Results come back as a single ranked list, each hit annotated with itschain_key. -
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.
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:
-
Per-team under a shared root.
Root:
org-shared. Teams:team-backend,team-frontend. Project branches:team-backend/auth-rewrite-2026q3. -
Per-tenant under a tenant root.
Root:
tenant-acme. Per-agent branches:tenant-acme/agent-codereview. Cross-tenant root is never queried — that's the security boundary.
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.
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:
org-shared,org-policy— root chains, never deletedteam-<name>— long-lived team chainsteam-<name>/<project>-<quarter>— bounded project branchestenant-<id>,tenant-<id>/<agent>— multi-tenant boundaryarchive/<original-key>-<date>— frozen, read-only history
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:
- Use per-tenant chain keys with a strict prefix (
tenant-acme,tenant-globex). - Enforce at the application layer that a tenant's session can only federate over chains sharing its prefix.
- Never branch a tenant chain from another tenant's chain. A cross-tenant
BranchesFromwould let the ancestor-walk leak data. - For hosted offerings, run the MentisDB server behind a proxy that injects the tenant prefix into every request — don't trust clients to scope themselves.
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.