If the model saw it, the log has it
You compact a long agent thread. The next turn feels lost.
The screen still shows a plan you approved ten minutes ago. The model acts like it never saw that plan. A tool result that changed the work is visible in the timeline, then missing from the next request.
The UI and the model have forked. That is not a model-quality problem. It is a history problem.
The rule
Model-visible means logged.
Anything that reaches a model request must be reconstructable from one append-only session log. If it is not in the log, it did not happen not for the model, not for replay, not for the timeline you show a person.
Once you take it seriously, a lot of agent bugs start to look like the same bug.
A chat transcript is not the history
Most products persist a list of messages: user, assistant, user, assistant.
That list is a view. It is not the history.
Compaction is a fact. Someone summarized the first forty turns and kept a tail. If you rewrite the message list and forget to record that rewrite, you cannot answer a simple question: what did the model see on the turn after the summary?
A tool call is a fact. The arguments, the result, the denial. If the model saw a tool result, that result belongs in the log — not only in a side cache the UI reads.
A role handoff is a fact. The builder took over from the planner. The next request is a different agent with a different job. That change is not a chat message. It still has to be in the history.
The plan a person approved is a fact. Teams often park it in a side store so summarization does not erase it. That is a symptom. The plan survived because the transcript was not trusted to keep it.
When those facts live beside the transcript, summarization and the UI slowly fork. You patch a cursor. You special-case “do not compact this.” You rebuild the model window from three places and hope they agree.
They will not keep agreeing.
Three sources. Two readers. No single answer to “what just happened?”
Example: an approved plan after compaction
A person asks an agent to add a refund path: check the invoice, then email the customer.
For twenty turns the agent researches, drafts a three-step plan, and the person approves it. The thread is now long, so you compact. The next message is: “Implement step two.”
Forked history. The model request is built from a summary plus the last few chat bubbles. The approved plan lives in a side store so summarization does not erase it. The UI still shows the plan. The model never sees it. It invents a new step two.
Logged history. The approval is an event. Compaction is an event: this range was summarized, here is the summary, here is the tail. When you build the next request, you project from the log. The approved plan is still in that projection, because it is a fact in the stream, not a paragraph that happened to survive in a bubble.
Same product. Same compaction. The difference is the write path.
What a session log is
A session log is a typed stream you only append to.
User message. Assistant message. Tool call. Tool result. Compaction. Role change. Plan revision.
Each event is a fact with a time and a type. You never edit the past. You add a new event that says what changed.
The model history is a projection of that stream: take the events that are allowed to reach the model, in order, and render them as messages.
The UI is another projection of the same stream: take the events a person should see, and render a timeline.
Replay is the same projection run later. “What did the model see?” is no longer a forensic question. It is a function of the log.
If you cannot rebuild the last model request from the log alone, you do not know what the agent knew.
Persist first, then derive
Write the event before the model reads it. Then build the request from the log.
Walk one turn:
A person sends a message. You append it. The model calls a tool. You append the call, then the result. Only then do you build the next request — from the log, not from an in-memory array you hope still matches disk.
Compaction is an event, not a rewrite the loop has to remember. The event says: this range was summarized, here is the summary, here is the tail that remains. The next projection uses the summary and the tail. You do not keep a cursor in the loop that has to be repaired after every splice.
New facts stop needing side stores. If the model should see the approved plan after compaction, the plan is in the log. Compaction cannot drop a fact the projection still needs, because the fact is not hiding in a chat bubble that got summarized away.
This is not a new database. Keep Postgres, files, whatever you already use. Change the write path so there is one history. Views can be many. The log is one.
What this is not
This is not a call to rebuild your product as a local coding agent.
You can keep workflows, tenants, billing, and a console that authors those workflows. The session log is the agent’s source of truth, not a replacement for the artifact your product exists to run.
It is also not a plugin framework. You do not need a new runtime to append typed events and project them.
You need one rule, applied every time something new reaches the model: write it to the log, then let every reader model, UI, replay derive from there.
The sentence to take with you
If you cannot rebuild the model request from the log, you do not know what the agent knew.
We are applying this to Ballet’s agent console: one history for what the model saw, what the person saw, and what we can replay.
