Imagine yesterday's repository review had made real progress. Several child Agents had inspected authentication, storage, and the job queue. You were comparing two refactoring directions when you closed your laptop. The next morning, you reopen the conversation and ask: “Which tool result led us to that conclusion?”
A system that saves only the final summary may remember the conclusion but lose the evidence. A system that saves only the chat bubbles may not know whether a tool ran, which context was compacted, or what settings the model used.
This is a useful place to continue exploring DeepSeek Harness. The previous article examined how to organize several Agents. This one asks how their records support recovery, reconstruction, and a different line of investigation. The source remains pinned to the commit behind dsh-v0.1.1-rc.2. Our opening situation is hypothetical, not a report of an actual incident. Session implementation
Record what happened before deciding what to display
A Session in this implementation is first an append-only event log. User input, complete assistant messages, tool calls and results, turn starts, step endings, and streaming chunks can each leave their own records. They share a contiguous seq: the next event receives the current log length, starting at zero.
That adds a layer beyond saving a conversation as an array. Chat is a human presentation; events describe facts about execution. A streaming chunk may need preservation for replay or accounting, but recording it should not make the system send it to the model again as another complete answer. Event types, message projection
Session.append() also does not retain the caller's mutable object directly. It takes a lossless JSON snapshot and freezes the accepted event. If a tool hands over a result object and later reuses that object, recorded history should not change with it. Invalid data, sequence relationships, or surface metadata must fail before admission, rather than becoming a surprise during a later storage write. Append entry point, JSON snapshots
Reading session.events returns a frozen array snapshot. Subsequent appends do not silently lengthen an array already held by a reader. “Which history am I analyzing?” therefore has a stable answer. This freezing constrains object use; it is not cryptographic tamper protection and does not replace storage permissions or backups.
One log, two ways to read it
The log keeps growing, but the model's context cannot grow indefinitely. Deleting early messages would remove evidence. Sending everything every time becomes increasingly expensive. Harness maintains a surface over the original events: think of it as an ordered directory of messages the model should currently read.
The directory stores event sequence numbers, not a second copied conversation. Three event types can produce messages in this version: user/message, assistant/message, and tool/result. They must specify how they enter the surface. Turn boundaries and streaming chunks do not enter it. deriveMessages() follows this directory, skipping assistant messages with empty content. Surface rules

Suppose two earlier module analyses are very long. A compaction plugin can append a summary at the log's tail while declaring which range of the current surface it replaces. The original events retain their positions, and the summary receives a new sequence number. The directory changes; the system does not rewrite the past to pretend the summary was all anyone ever said.
Both replacement endpoints must exist in the current surface and appear in the right order. sourceEventSeqs must cover every shadowed node. This records a structural relationship between the summary and its sources. Complete coverage does not establish that the summary is faithful, however; content quality needs separate validation. A tool-result replacement is more restricted: it can change the content of exactly one current tool result, without changing its call identity or other fields. Replacement validation
This also explains why the model surface is the wrong direct source for a human transcript. Once compaction lands, content the user saw yesterday would disappear from the page. The source provides isAppendSurfaceEvent() to distinguish original appended messages from replacement copies. Human transcripts should project the former, while model-facing consumers read the current surface. The same event source serves two different reading purposes.
Caching does not make every read constant-time
deriveMessages() remembers how many surface nodes it has processed. Without a replacement, it projects only new nodes, avoiding repeated parsing from the beginning of the raw log. A replacement changes replaceGeneration, clears the projection cache, and rebuilds it from the new directory. Incremental derivation
There is a cost hidden by the word “incremental,” though: the function returns [...this.derived]. Even with just one new message, it creates a new array covering the complete current history. With M visible messages, copying that returned array still costs O(M). The optimization removes repeated projection work; it does not make the entire call constant-time.
Replacement has a cost too. The implementation locates endpoints with indexOf and updates the directory with splice. This is not a balanced tree for arbitrary historical editing. The structure is direct and makes ordinary append behavior easy to maintain, but range searches, array movement, and projection rebuilding still need evaluation at the relevant scale. This version chooses a clear ordered directory without promising every history operation is cheap. Directory update algorithm
Recovering the conversation is not recovering the whole request
Return to the question you ask the next morning. Even if every message is recovered, the system prompt, tool definitions, and model-call settings are still missing. The same instruction to analyze something can mean different things under different tools or model settings.
The log therefore also contains request/header. It stores a canonical snapshot of the non-history request fields, and foldRequestHeader() selects the latest snapshot in the sequence. Messages come through the surface; system text, tool schemas, and call configuration come through the header. Complete assistant messages also retain provenance such as the provider and model that produced them. Request-header folding, Session data model
This provides evidence for reconstructing inputs, not a promise that a model will produce an identical answer. A newly resumed turn still composes its current request configuration. Changes to tools, models, or the environment can affect what happens next. The log helps explain the earlier input; it does not freeze the outside world in yesterday's state.
Between recording in memory and saving durably
We have repeatedly said “log,” but a successful append() first means an event has entered the in-memory Session. It does not wait for disk I/O for every streaming chunk. Persistence plugins subscribe to session/event, buffer records, and hand them to a backend.
A caller needing an explicit durability checkpoint uses ctx.sessions.flush(session). It starts all relevant listeners, waits for every listener to settle, and only then reports failure. One listener failing early does not cause the caller to stop waiting for the others. Its return value also indicates whether at least one persistence listener participated. Returning successfully with no listeners cannot be interpreted as proof of a disk copy. Flush implementation
The JSONL backend batches events, with a default waiting window of 200 milliseconds. The first pending event starts the window; later arrivals join it without repeatedly moving its deadline. This bounds intentional batching delay, not total durability latency. Backend I/O and event-loop scheduling still take time. Persistence design
There is a small naming trap as well: the default file is compressed .jsonl.zstd. It is logically JSONL, but its physical representation is not necessarily readable as plain text lines. Compressed and uncompressed modes share logical event semantics. This article makes no compression-performance claim and includes no power-loss or disk-fault experiment. JSONL backend
Did yesterday's write actually happen?
If interruption occurs during a tool operation, recovery cannot simply treat the incomplete transcript as a finished turn. interruptedTurnClosers() scans the loaded log, tracking the open turn, open step, and assistant-requested tool calls without results. When repair is needed, it generates tool error results first, then a step ending, and finally a turn ending marked interrupted. Recovery algorithm

The significant detail is the distinction between two kinds of uncertainty. If an assistant message requests a tool but there is no corresponding tool/call, the synthetic result carries TOOL_NOT_STARTED. If startup was recorded but no result became durable, it carries TOOL_OUTCOME_UNKNOWN.
Imagine the operation created a remote ticket. No result does not mean no ticket exists. The remote system may have succeeded while the local process failed before recording the response. The recovery message therefore advises deciding whether to retry from the tool's semantics and checking external state before repeating an operation with possible side effects. This reflects what the log can establish, rather than pretending uncertainty has disappeared.
Repair events reuse the last real event's timestamp and continue its sequence numbers. A balanced log produces no further closers. The JSONL backend's load() makes necessary repairs durable; inspect() can instead return a balanced in-memory view without changing the file. Viewing history and restoring a state suitable for continued execution therefore deserve separate operations. Repair function, load and inspection
Try the other path from yesterday's fork in the road
Finally, you decide to preserve the original conclusions while exploring a different storage design. SessionStore.fork() selects a prefix from a live Session, uses it as the seed of a child, and records lineage such as parentSession and seedLength. Its boundary is an inclusive event sequence number, not an exclusive array-slice endpoint. Fork implementation
That prefix must end outside an open turn. Cutting at the current tail while tools are still running would copy unclosed execution relationships, so it is rejected. The parent can nevertheless be busy with a newer turn while you explicitly select an earlier, completed boundary. Forking does not require the entire parent to be idle at that moment.
Parent and child subsequently append independently; child records do not grow into the parent. This is not a process checkpoint. It does not copy running child Agents or fork the database, working directory, or remote side effects. This version also requires the source to be the live instance in the store; an ID for a persisted but unloaded session is insufficient. It provides stable-prefix branching, not an arbitrarily editable tree of conversation entries. Fork boundaries and limitations
For this article, 22 checks executed the pinned Session, surface, JSON snapshot, header-folding, and repair logic. They covered immutable snapshots, replacement provenance, reconstruction, fork boundaries, and both interrupted-tool outcomes. Framework and freezing helpers were substituted, and store publication used a simplified adapter. No complete Agent, real model, filesystem fault recovery, or end-to-end resumed conversation was exercised.
“Does it remember?” now becomes a set of more precise questions: do the original facts remain, how is current context selected, what has reached storage, and what must remain unknown? The next article follows the Agent loop and how these records connect to execution, cancellation, and permissions.

