Your agent's memory needs an address
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.
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.