Intelligence stopped being the blocker. Kimi K3 put 2.8 trillion parameters of open weights on Hugging Face this month. GLM 5.2 sits at the top of the coding arenas at a fraction of frontier pricing. DeepSeek V4 takes a million tokens of context. Any of them is smart enough for the work most companies actually want done, and if none of them are, a frontier lab ships something in six weeks.
What stops the agent you want to deploy is context. The model does not know your customers, your pricing exceptions, or which of two contradictory Slack messages is the current one.
Most agents answer that with a filesystem. ChatGPT keeps a running summary of you across chats, Anthropic’s memory tool is literally a directory the model reads and writes between sessions, and coding agents keep a repo of markdown under AGENTS.md. For one person that is the right answer, and I would not replace it.
It gives you no schema, though. Nothing stops two files from disagreeing about a customer’s plan. Nothing marks which one is current. Nothing records who wrote either. That is fine on one laptop. It is not how you run a company.
Companies solved this once already, by putting a warehouse in the middle and making it the thing everyone reads. I have argued that analogy in full in The Modern Data Stack for Agents, and the split between the notebook and the warehouse in Filesystem vs Database for Agent Memory. This post is about what the storage underneath has to guarantee, because an agent only improves when its past is queryable. Retraining, a rewritten prompt, an eval set: each one needs a record of what the agent did, what a human said about it, and what happened next.
So Lobu’s memory layer is closer to git than to a memory API. Every write is an append. Nothing is edited in place. Correcting a fact means writing a new one that points at the fact it replaces. History is walkable, provenance is a column, and human review is a merge that happens before the write lands rather than a cleanup afterwards.
The log is not another system of record. Slack owns the conversation, Stripe owns the payment, the warehouse owns revenue, and mirroring them into a second authority buys you a reconciliation problem you did not have. Our log is a proxy: an append-only record of observations about systems we do not own. The sources stay authoritative for the facts. The log is authoritative for what we observed, when, and what every agent did about it.
A memory write is a commit
The agent-facing surface is one MCP tool, save_memory, and its contract is the whole model in a sentence: storage is append-only, pass supersedes_event_id to replace an existing fact, and the old event is hidden from future searches without losing history.
Agents are told to use it that way. The guidance we ship reads: “When a message changes a fact you already stored (an updated preference, status, count, location, or plan), first search for the prior memory to get its id, then save the new value with supersedes_event_id set to that id.”
None of this relies on the agent behaving. The database only lets one event replace a given fact, so if two agents try to replace the same one at the same moment, the second gets an error rather than quietly creating a second version of the truth.
So “what is true right now” is not a field anyone maintains. A fact is current if nothing has replaced it. That is the whole rule.
Ask for a fact’s history and the server walks that chain in both directions and hands back every version.
The workspace Lobu runs its own company on holds roughly 230,000 events, about 27,000 of which replace an earlier observation. You keep every version, and most of them nobody will ever read. That sounds wasteful right up until somebody asks what the agent believed on Tuesday.
Syncs pick up where they stopped
Facts arrive through feeds. A feed is one connector-defined sync against one connection, and it carries a checkpoint the connector moves itself. The next run starts where the last one stopped instead of re-reading the source.
The connector does not have to think about duplicates. We catch them at the insert, keyed on the connection plus the source’s own id, and we lock per item, so two syncs of the same Slack thread queue behind each other while unrelated items run in parallel.
That is what lets a connector be dumb. Running a sync twice costs a little time and changes nothing.
Behaviors get the same property from the log. A Behavior’s outstanding work is a subtraction: every event, minus the ones already linked to a run it finished. There is no cursor to corrupt and no watermark to reset. If a run fails, its events were never linked, so the next one picks them up.
Behaviors are mutations on your data model
A Behavior is three things: a trigger, a prompt, and optionally some named SQL over the log. The trigger says when. The prompt says what matters. The SQL is how it reads what a connector wrote, and a typed reaction is where it acts. Every run records exactly which events it consumed.
Whatever it produces goes back into the same graph, in one of two shapes. It appends an event: a memory, a summary, a notification, a request for approval. Or it changes an entity: a company’s stage, a member’s role, a deal’s amount.
That is the whole vocabulary. A Behavior is a write against your data model, with a language model in the middle deciding the arguments.
The Behaviors page for an agent, listing three behaviors. The first is expanded to show its triggers — two GitHub events and a scheduled run — the prompt it runs, and what it writes: a change to the Issue entity and a new event appended back to the log
That loop needs one guard, and we found it the hard way.
A Behavior with no explicit source used to read every event. Our own bookkeeping rows were invisible to it, but only by accident: we were writing them without a timestamp. The day we started filling that column in, every Behavior began reacting to its own audit trail.
The default source now skips those rows. The migration matched them by exact text, so nobody’s hand-written SQL was touched.
The human review step is a merge
Fields carry ownership. Once a human sets one, it is theirs.
Humans and agents share a single write path, and it behaves differently depending on who is calling. A human write sets the value and claims the field. An agent write only lands on fields nobody has claimed.
A blocked field does not get overwritten, and it does not fail quietly either. Every entity write runs through a gate that can allow it, deny it, or hold it. A held write becomes a pending run plus an approval event, queued only after the caller’s transaction commits, so an approval never rides on a write that might roll back.
The approval is more than a yes or a no. The event carries the JSON schema of the input the agent proposed, so the app renders it as a form and the reviewer edits the arguments before approving. One run proposing twenty field changes collapses into one card. The same event renders as a Slack card with buttons, and both paths call the same tool underneath.
Which actions need a human at all is operator configuration, not something the agent decides.
The Lobu activity page filtered to pending approvals: a Create Issue action requested by an MCP client, its arguments rendered as an editable form with title, labels, repo name and repo owner fields, and confirm and reject buttons
A rejection is not the end of it. When a human corrects a Behavior’s output, the correction is an event too, and the next run of that Behavior gets the latest correction per field folded into its prompt. Older ones drop out, so the prompt does not accumulate the whole argument.
The human is changing what the agent does next by editing the record. It is the only feedback channel we have that survives swapping the model.
Every run writes to the same log
Agents still get a filesystem. Each conversation gets a workspace for scratch work: downloaded PDFs, intermediate CSVs, generated reports. That layer is meant to be thrown away.
What leaves it is the write to the log, and every one of those names the run that made it: the Behavior that fired, the agent it belongs to, and the person who started it if a person did. Roughly 90,000 of those 230,000 events name their run.
The same is true of agents we do not run. Connect ChatGPT to the Lobu MCP server and every tool call it makes writes an audit event: which agent, which session, which client, which user, the tool name, a hash of the arguments, and a redacted preview. We record the shape of the call and never its content.
On Slack a thread is a conversation in its own right, pinned to one agent. It is a durable run, not a session that dies with the socket.
The Lobu activity feed showing mixed rows: a tool invocation audit event with agent id, category audit, event type tool_invocation.completed, an MCP session id and the tool name, a config change event for an inference provider, and a rejected Create Issue action requested by an MCP client
So you end up with one audit trail across the agents you run and the ones you do not, rather than one per vendor. “Why does this company say Series B” resolves to a run, the events that run read, and the human who approved the change.
Where the analogy stops
There is no force-push and no rebase. A fact we mirrored can change under us, and the honest record of that is a new observation pointing at the old one.
There is no revert either. You cannot un-send the email. That is why the gate sits before the write, and why a correction is an append.
None of this is clever, which is the part I would defend. The model in the loop gets better a few times a year when a lab ships something, and that has stopped being the interesting variable. The record underneath gets better every week.
Start with the memory layer, or read how Behaviors are defined.