← All articles Agent infra

Your agent's memory needs an address

20 August 2026 · 7 min read

Between two tool calls, an agent on OBTO remembers nothing at all.

Our platform used to hold a session. The server tracked an active app and an active tenant, and every tool call inherited that context for free. We took it out in 3.3.0. Every call now carries its own appName and domain, because a scope the server remembers is a scope another conversation can inherit.

That fixes drift and creates a smaller problem in its place. An agent halfway through a long task knows things: which tenant it is working for, which decision it already made and should not revisit, which of three approaches the person rejected on Tuesday. None of it survives the end of the turn.

The reflex is to widen the context window. That holds until the run is longer than the window, or the process restarts, or the person comes back a week later in a different client. A context window is a buffer, and the agent needs somewhere to write.

The scope key is the whole design

A memory store for agents is easy to build badly. Give an agent a vector database and a write tool and it will use them, and by default it will write a memory during a task for one tenant and recall it during a task for another. The embedding matched. Nothing else was asked.

Every serious vector store can prevent that. Pinecone has namespaces, Qdrant filters on an indexed payload field, Weaviate puts multi-tenancy in the data model. The primitive exists. What none of them can know is which tenant your agent was acting for at the moment it called the write, because that fact lives in your application rather than in the store. Somebody has to carry it across, on every call, forever.

So a memory write on OBTO establishes its address before it establishes its content. obto_remember resolves to one of two scopes from the arguments you pass, and tells you in the response which one it picked.

App-scoped memory is keyed by (userId, appName, domain). This is durable knowledge about a thing: how this app is put together, a quirk in its data, a preference that should still hold in six months. It outlives every conversation that touches the app.

Conversation-scoped memory is keyed by (userId, conversationId), where the conversation ID is a token the caller generates. It is scoped to one job rather than one application.

Giving a small model somewhere to put its context

A frontier model can hold a long task in its head. A small model wired into a product API has a much harder time of it. The usual workaround is to replay the transcript every turn and hope the load-bearing line survives truncation.

Conversation-scoped memory is the other option. At the start of a turn the agent writes down what it just established. At the start of the next turn it reads it back.

// end of turn: write the state the next turn needs
obto_remember({
  conversationId: "job-4471",
  key: "active_scope",
  content: "app=invoices domain=acme; user approved the write path"
})

// start of next turn: read it back
obto_recall({ conversationId: "job-4471", query: "active scope", k: 5 })

The context window stops being where the agent's state lives and becomes a record of what it loaded this turn. Under a stateless contract that is a reliability gain, because smaller models do better when nothing is implicit. Nothing is left sitting on the server to drift. There is a record with an address, and the agent either read it or it did not.

What the platform owes the agent back

Passing a key turns the write into an upsert. Call obto_remember again with the same key in the same scope and the prior content is replaced. Leave the key off and every call inserts a new entry.

Something like active_scope should be keyed, because there is exactly one correct answer and yesterday's is wrong. A log of decisions should not be, because the history is the point. Getting it backwards gives you a store that either forgets what it was supposed to keep or accumulates six stale copies of one fact and returns the wrong one on similarity.

Recall fails soft. If the memory store is unavailable the call returns {ok: true, returned: 0} with a note explaining why, rather than throwing. An agent loop that dies because its memory is down is worse than an agent that proceeds without it. A miss and an outage are still distinguishable in the response, so the agent can tell the difference when it matters.

Part of the address is not the caller's to choose. The user dimension comes from the authenticated identity and has no parameter, so one person's memories are not reachable by asking for them differently. The rest of the scope (app, domain, or conversation) is supplied per call, the same way every other tool call on the platform carries its own.

Worth being precise about, because this is a weaker guarantee than the one on our database reads, where tenancy fields are injected into the query and overwrite whatever the agent sent. Memory is partitioned by an address you pass. What the platform adds is that it hands the address back: every remember and recall returns the mode it resolved and the projectKey it used.

That is the difference worth having. A tenant filter threaded through every call site is one refactor away from being dropped at one of them, and a dropped filter fails quietly, returning results that happen to be the wrong tenant's. Putting the scope in the tool contract does not prevent that mistake, but it does shorten how long the mistake stays invisible.

When to write something down

Memory earns its cost when it stops a decision from being made twice. Write when the agent establishes something a future run would otherwise have to rediscover: a scope, an approval, an approach that was tried and rejected, a quirk in the data. Skip the running commentary. A store full of "started working on the task" returns noise on similarity search, and the top few hits are all the agent gets.

Scheduled runs are the clearest case. Nobody is watching a 3 a.m. job, and the next one starts cold. Whatever the last run learned either got written down or it did not happen.

Which is also why memory and the audit trail need to stay separate. Memory is what the agent chose to keep. The audit trail is what it could not avoid producing. Anything the agent can decide not to write is not an audit trail.

Frequently asked

What is AI agent memory?

Agent memory is durable storage an agent writes to on purpose and reads back later, separate from the context window. The context window holds one turn and disappears. Memory survives a session reset, a process restart, and a change of client, so a later run can recall a scope, an approval, or a decision that an earlier run established.

Why not just use a bigger context window?

A larger window delays the problem rather than removing it. Long-running tasks outgrow any window, processes restart, and a person can return a week later in a different client with no transcript. A context window is a per-turn buffer. Anything that must outlive the turn needs to be written somewhere addressable.

Should agent memory be scoped to the app or to the conversation?

Scope to the app for knowledge about the application itself, such as its architecture, quirks in its data, or a preference that should hold months from now. Scope to the conversation for working context inside a single job, such as the active scope or an approval the user just gave. App-scoped memory outlives every conversation; conversation-scoped memory ends with the job.

How is agent memory different from an audit trail?

Memory is what the agent chose to keep, so it is selective and under the agent's control. An audit trail is a record the agent cannot avoid producing, generated by the platform on the write path. They answer different questions and should not share a system, because anything the agent can decide not to write is not an audit trail.

Can a small model manage its own memory reliably?

It can when the contract is explicit. A small model struggles to carry state implicitly across turns, which is why replaying transcripts is fragile. Writing state to a keyed record at the end of a turn and reading it back at the start of the next one turns that from something the model must remember into a tool call it either made or did not.

What happens if the memory store is unavailable?

Recall on OBTO fails soft. When the store cannot be reached the call returns a successful response with zero results and a note explaining why, instead of throwing. An agent loop that dies because its memory backend is down is worse than one that proceeds without recall, and the response still distinguishes an outage from a genuine miss.

More from the OBTO blog