I watched an agent onboard itself to an unfamiliar payments service four times in one afternoon. Same service, four fresh sessions, four times it traced the webhook handler from scratch, re-discovered that retries were idempotent because of a dedupe key, and re-concluded that the legacy_ prefix on three functions meant "do not touch." Every session it did good work. Every session it threw that work away when the context window closed.

The agent had a map. It could search the codebase and find anything. What it didn't have was a memory. So it kept re-drawing conclusions it had already drawn, because a map tells you where things are — it doesn't tell you what you decided about them last Tuesday.

That's the gap this post is about. Semantic code search and persistent memory solve two halves of the same problem, and most people run exactly one of them. Here's why you want both, and how to make them compose.


Two failure modes, one missing half

Give an agent search without memory and you get the four-times-an-afternoon loop. It can locate the webhook handler instantly, but the meaning of that handler — why it's structured the way it is, what's safe to change, which path is the legacy one — lives nowhere durable. Every session re-derives it. The agent is competent and amnesiac.

Give an agent memory without search and you get the opposite failure, which is quieter and worse. It remembers the conclusion — "the dedupe key in the webhook handler makes retries idempotent" — but six weeks later it can't relocate the code that conclusion is about. The handler got refactored, the function got renamed, the file got split in two. The memory is now a confident sentence pointing at nothing. The agent trusts it and acts on a fact that's no longer true.

The two tools cover each other's blind spots:

Finds code Remembers decisions Without the other
Semantic search yes no Re-derives context every session
Persistent memory no yes Remembers conclusions it can't relocate

A map and a memory. Run one, you're missing a half.

At Muvon we ship both halves as open-source MCP servers — Octocode for the map, Octobrain for the memory — and the host that runs them together, Octomind. Both are Apache-2.0. This post is the workflow guide for using them as a pair. If you want the deep dives, Octocode's semantic code search and introducing Octobrain cover each tool on its own. I'll reference rather than rehash.


What each side actually exposes

Grounding first, because the composition only makes sense if you know the real tool surface. These are the MCP tools, not a wish list.

Octocode indexes your repo with tree-sitter AST parsing — real symbols, not flat text chunks — and exposes four tools to the agent:

Tool What it does
semantic_search Recall-oriented search by concept or behavior. "Where is authentication handled", "code that retries failed requests." Finds code even when the names don't match your words.
structural_search AST pattern matching and exact symbol lookup. Precise and cheap when you know the name, the string, or the call sites.
view_signatures Extract function signatures and type definitions without the bodies — the cheapest way to map a file before reading it.
graphrag Knowledge-graph queries over imports, calls, implements, extends. "What depends on the payment module."

The index is project-scoped and local-first. You build it once with octocode index and it stays current. The point worth holding onto: Octocode answers "where is this and how is it wired?" — and only ever reflects the code as it exists right now.

Octobrain gives the agent long-term memory, scoped per project by its normalized Git remote URL (host/org/repo). Four MCP tools:

Tool What it does
memorize Store an insight, decision, or fact. Takes title, content, a memory_type (architecture, decision, bug_fix, security, …), importance, tags, related_files, and related_to[] for inline links to other memories.
remember Semantic search over stored memories. Returns 1-hop graph neighbors automatically. Supports created_after / created_before for temporal queries.
forget Delete a memory by memory_id or by query — irreversible, requires confirm=true.
knowledge A separate knowledge base for indexing and searching external docs and URLs (search, store, read, match, delete).

The memorize tool's own description tells the agent to call remember first to avoid duplicates, and to mark facts as user_confirmed (high importance) versus agent_inferred (lower). Octobrain auto-links semantically similar memories Zettelkasten-style, and supports supersedes relationships so a corrected fact ranks above the stale one it replaced — the old one stays queryable for history. Hold onto this: Octobrain answers "what did we decide about this, and when?" — and it's the only side that persists across sessions.

Notice the symmetry. Octocode knows the current code but forgets every conversation. Octobrain remembers every conversation but doesn't know the code. Neither field — related_files on a memory, a file path in a search result — is meaningful without the other tool to resolve it.


How they compose: the loop

The two tools don't just coexist — they form a loop, and the loop is the whole technique. Four moves:

1. Search to locate. The agent doesn't know where the webhook idempotency logic lives. It calls semantic_search("webhook retry idempotency dedupe"). Octocode returns the handler and the dedupe-key check. The agent now has a location and current code.

2. Memorize the decision, not the location. Having reasoned about that code — confirmed retries are idempotent, identified the legacy path, agreed with you on a scope boundary — the agent calls memorize. Critically, what it stores is the conclusion and the reasoning, tagged with related_files, not a copy of the code and not a line number. memory_type = "architecture", importance high if you confirmed it. The decision is now durable.

3. Remember next time. New session, context window empty. Before touching anything, the agent calls remember("webhook payments idempotency"). Octobrain returns the stored decision plus its 1-hop neighbors — the related security note, the linked legacy-path warning. The agent starts the session already knowing what took four sessions to figure out before.

4. Re-search to verify. This is the move people skip, and it's what keeps memory honest. The memory says "idempotency lives in the webhook handler." Before acting on it, the agent calls semantic_search or view_signatures on the related_files to confirm the code still matches the memory. If the handler was refactored and the dedupe key moved, the re-search surfaces the mismatch. The agent updates the memory — memorize with a supersedes link to the old one — and proceeds on current truth.

        ┌─────────────────────────────────────────────┐
        │                                             │
        ▼                                             │
  semantic_search ──► reason ──► memorize ──► remember
   (locate code)    (decide)   (store the    (next session:
        ▲                       decision)     load it back)
        │                                          │
        └────────── re-search to verify ◄──────────┘
            (does the code still match the memory?)

The map keeps the memory anchored to real code. The memory keeps the agent from re-deriving what it already knows. Search without step 2 and 3 is the amnesiac agent. Memory without step 1 and 4 is the confident-but-wrong agent. The loop is both halves doing their job.

The reason step 4 matters so much: a memory is a claim about code at a point in time, and code moves. Octobrain can rank a supersedes-corrected fact above the stale one, but something has to notice the staleness first. Octocode is that something. The re-search is the agent's reality check against its own memory.


Registering both servers so the agent has them together

Neither tool helps if the agent can only reach one. The whole point is having the map and the memory available in the same session so the agent can run the loop without you brokering it. Here's the wiring.

Octomind declares MCP servers in its config under [[mcp.servers]]. The built-in servers (core, runtime, agent, orchestration) are always there; you add Octocode and Octobrain as two stdio servers:

[[mcp.servers]]
name = "octocode"
type = "stdio"
command = "octocode"
args = ["mcp", "--path=."]
timeout_seconds = 240
tools = []

[[mcp.servers]]
name = "octobrain"
type = "stdio"
command = "octobrain"
args = ["mcp"]
timeout_seconds = 60
tools = []

tools = [] means "expose all of this server's tools." --path=. scopes Octocode to the current repo; Octobrain scopes itself automatically by the normalized Git remote URL (host/org/repo), so the memories an agent stores in one project stay isolated from another. Octocode's timeout is generous because the first search after an index can pull a local embedding model into cache; Octobrain's is shorter because memory lookups are cheap.

If you're on Claude Code instead of Octomind, it's the same two servers, registered the CLI way:

claude mcp add octocode  -- octocode mcp --path .
claude mcp add octobrain -- octobrain mcp

Claude Desktop, Cursor, and Windsurf take the same mcpServers JSON block — one entry per server. The mechanics are documented per tool; the only thing that matters here is both, in the same agent. An agent with Octocode but no Octobrain is a map with no memory. An agent with Octobrain but no Octocode is a memory with no map. You want the pair.

One prerequisite before the first run: index the repo. octocode index in the project root. Octobrain needs nothing — it starts empty and fills up as the agent stores decisions. If your repo is large enough that indexing is a real operation, the sibling post on indexing a large codebase for semantic search locally covers doing it without API keys.


Worked example: onboarding an agent to an unfamiliar service

Here's the payments service from the top of the post, run properly this time. Watch the loop do the work that four amnesiac sessions couldn't.

Session 1 — cold start, build the map and the first memories.

The agent has never seen this service. It starts by mapping, not reading:

view_signatures("src/payments/**/*.rs")
  → webhook.rs: handle_webhook, verify_signature, dedupe_key
  → charge.rs:  create_charge, capture, legacy_settle, legacy_refund, legacy_void

Three legacy_-prefixed functions jump out. The agent asks; you confirm they're a deprecated settlement path that a cron job still calls and nobody is allowed to "clean up." That's a decision worth keeping — so it goes into memory, not into the void:

memorize(
  title: "legacy_ settlement path in charge.rs is load-bearing",
  content: "legacy_settle/refund/void are called by the nightly
            reconciliation cron. Deprecated but NOT dead. Do not remove
            or refactor without checking cron/reconcile.rs.",
  memory_type: "architecture",
  importance: 0.9,
  source: "user_confirmed",
  tags: ["payments", "legacy", "cron"],
  related_files: ["src/payments/charge.rs", "src/cron/reconcile.rs"]
)

Then it traces idempotency with semantic_search("webhook retry idempotency"), lands on dedupe_key, reasons through it, and stores a second memory — memory_type: "architecture", linked to the first with related_to so the two surface together later. Session ends. Two memories persist; the code index persists. The afternoon's reasoning is no longer disposable.

Session 2 — warm start, three weeks later.

Fresh context window. Instead of re-tracing, the agent loads what it knows first:

remember("payments webhook legacy settlement")
  → "legacy_ settlement path is load-bearing" (importance 0.9, CONFIRMED)
  → 1-hop neighbor: "webhook idempotency via dedupe_key"

One call, and the agent starts where session 1 ended — including the linked neighbor it didn't explicitly ask for. Now the verify step. The memory points at src/payments/charge.rs, so before trusting it the agent re-searches:

view_signatures("src/payments/charge.rs")
  → create_charge, capture, settle_v2, refund_v2, void_v2

The legacy_ functions are gone. Someone shipped settle_v2 and deleted the legacy path. The memory is now stale — and because the agent verified against the map instead of trusting the memory blind, it caught it. It supersedes the old decision rather than acting on a fact that's three weeks expired:

memorize(
  title: "settlement path migrated to settle_v2",
  content: "legacy_settle/refund/void removed in the v2 migration.
            cron/reconcile.rs now calls settle_v2. Earlier 'do not remove'
            note no longer applies.",
  memory_type: "architecture",
  related_to: [{ target_id: <old_memory_id>, relationship_type: "supersedes" }],
  related_files: ["src/payments/charge.rs", "src/cron/reconcile.rs"]
)

Future remember calls now rank the corrected fact above the stale one, while the old note stays queryable for anyone asking "what did this used to do?" The agent onboarded itself once, kept the result, and self-corrected when reality moved — which is exactly what the four-sessions-an-afternoon agent could never do, because it had a map and no memory.


Anti-patterns

The loop is simple. The ways to break it are specific. Avoid these:

Memorizing volatile line numbers. "The bug is at charge.rs:142" is worthless the moment someone adds an import above it. Store what and why, anchored with related_files and symbol names — let the re-search step relocate the where. Octocode finds dedupe_key whether it's on line 142 or line 90; a memorized line number is just a lie with a timestamp.

Over-indexing into memory. Octobrain's memorize description is explicit: skip transient state and things easily re-derived. If semantic_search can find it in one call, it doesn't belong in memory — memory is for conclusions and decisions, not facts the map already holds. Memorizing "the auth module is in src/auth" wastes a slot on something the map answers for free, and dilutes the recall of the memories that actually matter. Memory stores what the code can't tell you: the why, the scope boundary, the "don't touch this" you only know because someone said so.

Trusting stale memories without re-verifying. This is the one that bites hardest, and it's why step 4 exists. A memory is a claim about code at a point in time. Code moves. Always re-search the related_files before acting on an old decision; when the code has drifted, supersede the memory instead of forcing the old conclusion onto new code. A memory you never verify is technical debt that talks back.

Letting superseded memories rot instead of linking them. When a fact changes, don't just memorize the new one and orphan the old — link them with supersedes. Octobrain ranks the current fact above the outdated one and keeps the history queryable. Orphaned corrections leave two equally-confident contradictory memories and no way to tell which is current.

Running one half and calling it done. Search alone re-derives forever. Memory alone drifts out of sync with the code. The value isn't in either tool — it's in the loop between them. If you only register one server, you've built half a brain.


The shorter version

An AI agent in an unfamiliar codebase needs two things a human takes for granted: the ability to find the relevant code, and the ability to remember what was decided about it. Semantic search is the first. Persistent memory is the second. Run them as a pair — search to locate, memorize the decision, remember next time, re-search to verify — and the agent stops re-onboarding itself every session and stops trusting conclusions it can no longer place in the code.

A map tells you where things are. A memory tells you what you decided about them. Give the agent both and wire the loop between them. That's the whole technique. If memory hygiene is the part you want more on, the sibling post on agent memory without the noise goes deeper on what's worth keeping.

— Don


Octocode and Octobrain are open source under Apache-2.0, and run together inside Octomind. Found a sharp edge in the loop? Open an issue — the workflow gets better when people tell us where it breaks.