From LLM to HarnessPart 7/11

The log is the source, not a copy

Everything the model sees must be in the log first. Context is never stored beside it — it is derived, and that is the difference between a source and a copy.

A stack of log records with two arrows fanning out to what is derived from it
TL;DR — Everything the model sees must be in the log first. Context is never stored beside the log — it is derived from it. That is the difference between a source and a copy.

The ordering decision

There are two ways to write an agent, and they differ in the order of two operations.

The first: call the model, take the reply, then write down what happened. The log is a record of the conversation.

The second: write the event to the log first, then build context from the log and call the model. The log is the source.

The first is easier and fails in a way that is hard to see. If the log is a copy, then two places hold the truth — the array in memory and the file on disk. Every edit to one means remembering to edit the other. Forget once and the two diverge, and the divergence only shows up when you resume.

The harness I read chose the second and says so plainly:

A Session is an append-only log of typed SessionEvents — the single source of truth for an agent’s whole interaction history. The LLM message history is derived from the log, never stored separately; replay is re-derivation from the same events.— dsh, docs/subsystems/session.md

Three clauses. Append-only. Single source of truth. And the third is the one that decides: replay is re-derivation, not reading back a saved copy.

Appended does not mean durable

This is the detail I find most worth remembering here, and it is the one most of us get wrong.

append does not promise survival. Verbatim:

Persistence is best-effort: on resolution the batch is accepted, ordered, and visible to reads on this backend instance, but only a resolved flush promises it survives a crash — a backend may buffer or batch physical writes behind append.— dsh, docs/subsystems/persistence.md

Read that closely: once append resolves, the event is readable in this process. But it may still sit in a buffer. Only flush is the durability barrier.

The documentation gives flush a very clear name: the durability barrier. And it states the condition for use: Callers that must survive a crash flush.

Which means whoever needs to survive a crash must call it. Nothing does it for you.

This is the easiest thing to get wrong when building your own harness. You call append, you see the promise resolve, and you believe the data is safe. It is not safe. It is merely somewhere the current process can read.

Crash repair belongs to the reader

A log that crashed mid-turn ends with an open turn/start and no turn/end.

My first instinct was that the storage layer would trim that broken tail. Wrong:

Persistence does not truncate or repair it — a single turn can be huge in a long-horizon task (many steps, large tool output), and those events were durably appended before the crash.— dsh, docs/subsystems/persistence.md

The reason is practical. A turn may hold hundreds of already-durable events. Trimming it throws away good data just because one closing line is missing.

So the storage layer returns a physically valid contiguous log, and the repair belongs to the reader. On resume, the loop reads the stored log, computes the missing closing events, and appends them through the same handle:

the closing events computed on resume
interruptedTurnClosers // three kinds:
// - missing tool errors
// - any open step/end
// - a synthetic turn/end, reason: { kind: 'interrupted' }

And there is a notable constraint: repair only writes when you hold write ownership. Another process opening the same session for writing is rejected with SessionAlreadyOwnedError, rather than letting two parties race to repair.

Seven ways a turn can end

A turn does not end in only one way. There are seven branches, and two of them no loop ever emits:

BranchMeaning
completedFinished normally
abortedA cancellation request arrived
blockedStopped by policy
errorFailed, with structured failure facts
max-tokensSome step hit its output ceiling
interruptedA crash-orphaned turn, closed after the fact
forkedAn open turn closed at the fork boundary

The last two are where I stopped for a while. No loop emits them. interrupted is written by resume, or synthesized by the query layer on a cold read. And forked appears only in a fork seed.

The type is also merge-extensible — a plugin adds its own branch by merging into the map. So every switch over it must carry a default. The documentation gives the reason: a plugin-added variant is a valid unknown value.

One log, two projections

This is the most interesting consequence of a log-as-source design, and I have not seen it in another harness.

From the same log, there are two separate views:

  • Surface fold: Builds the message list sent to the model. When a section is compacted, the surface only keeps the new summary and hides older events.
  • Append-origin: Rebuilds the full history for humans to inspect. It keeps every original event intact, even those hidden from the model.

One single source of truth produces two distinct views for two different audiences.

One turn across five componentsThe loop coordinates everything by writing to the log, calling the model, and recording tool resultsUserThe LoopDurable LogModelTool
Figure 1 — One turn across five components · tusgino
View diagram data
type: sequence
title: One turn across five components
desc: The loop coordinates everything by writing to the log, calling the model, and recording tool results
nodes:
  - id: user
    label: User
    kind: input
  - id: loop
    label: The Loop
    focal: true
  - id: log
    label: Durable Log
    kind: store
    focal: true
  - id: model
    label: Model
    kind: external
  - id: tool
    label: Tool
edges:
  - [user, loop]
  - [loop, log]
  - [loop, model]
  - [model, loop]
  - [loop, tool]
  - [loop, log]

The price

There is no second copy to read quickly.

Every question about the past is a rebuild. You have no array to print, no table to query directly. To find out what the third step of the second turn contained, you run the fold over the log.

And because the log is append-only, you cannot edit the past either. To “delete” something already written, you append a replacing event — which is exactly how compaction works.

In exchange you get three things. One source of truth. A pure function from log to request. And the ability to replay exactly what happened.

Do this now

  1. Pick the order before writing the first lineWrite then call, or call then write. Choose the second and you already have two sources of truth, and you will be reconciling them forever.
  2. Separate append from flush in your APIDo not let one function both accept data and promise durability. Callers need to know which side of the barrier they are on.
  3. Let the reader repair, not the writerA crash leaves the log open. Trimming it throws away good data. Let the next resume compute what is missing and write it back.
  4. Name every way it can endSeven branches sounds like a lot, but each is a situation you will meet. Collapsing them into error loses the ability to tell a rare case from a common one.

The rule

Choosing the log as the source is choosing a constraint: no other place may hold the truth. That constraint is annoying at first and pays off later.

The log is not for debugging. The log is the context. If it is only a side record, you have two sources of truth and no idea which one is right.— tusgino
    • Write to the log before calling the model; do it the other way round and the log is a copy, and copies drift.
    • append is best-effort; only flush promises survival across a crash. If you need durability, you must flush.
    • The storage layer deliberately neither truncates nor repairs a broken log; repair belongs to the reader on resume.
    • Seven branches end a turn, and interrupted and forked are emitted by no loop.
    • One log, two projections: surface for the model, append-origin for the reader — and they differ on purpose.

Frequently asked questions

Why not keep the model history as its own array?
Because two copies drift apart. If the log is the source and that array is a copy, every edit to one means remembering to edit the other.
Has append written to disk yet?
Not necessarily. append is best-effort: it accepts the batch and makes it readable in this process, but only flush promises it survives a crash.
Who repairs the log after a crash mid-turn?
The reader. The storage layer deliberately does not truncate or repair it. On resume, the loop reads the stored log and appends the missing closing events itself.
Is the log just for debugging incidents?
No. The log is the context. The context sent to the model is derived from it, so the log is not a side record — it is the source of the request itself.
Tú (tusgino)Tú (tusgino)

AI Software Engineer in Da Nang. I write about production system architecture and what each choice costs.