Lesson 3: Agent Loop and Sessions: Everything Is Documented
In one sentence: DSH records the agent's entire working process as an append-only, never-modified session log — whatever the model sees, the log records. Recovery, forking, replay, telemetry, and the UI all derive from this single log, so even a task that is only halfway done can be continued, forked, or replayed losslessly.
1. User Story: How Do You Continue Losslessly When a Task Is Halfway Done?
Picture this: you ask an agent to migrate an old repository from Vue 2 to Vue 3. It has already been running for three hours — editing dozens of files, running several rounds of tests, and going back and forth with the model across hundreds of steps. Then three blood-pressure-raising things happen:
- Your computer suddenly restarts, and the process is gone;
- Or you change your mind: you want to try a more conservative migration path, but you don't want to throw away the progress you've already made;
- Or you want to replay it: which step exactly broke that config file yesterday?
In ordinary tools, these three things amount to "start over" or "search from memory." In DSH, all of them are lossless, precise, and fully documented:
| What you want | What DSH does | Result |
|---|---|---|
| Continue after a crash | Load the persisted session with resumeSessionId | Turn numbering and derived history continue from the loaded log, as if never interrupted |
| Try a different path | Fork a session | A parallel branch is copied from a stable checkpoint; the original session is completely untouched |
| Review history | Read the session log | Every model request, tool call, and result has a raw record |
This lesson clarifies one thing: why all of this is possible. The answer lies in one phrase — reconstructible at runtime.
2. Core Idea: The Session Is an Append-Only Event Stream, the Authoritative Log
Session = Source of Truth, Message History = Derived
First, look at how the dsh-session package defines itself (source: packages/core/session/README.zh.md):
Event-sourced session log and in-memory storage. The Session is the append-only source of truth for an agent's entire interaction history; the LLM message history is derived from it.
Let's unpack this sentence:
- Append-only: events can only be appended to the tail of the log; no one can go back and modify or delete records that have already been written;
- Source of truth: the session log is the single authority; the message history the model sees and the transcript shown in the UI are copies derived from this log, not a separate piece of state;
- Derived from it: you don't need to maintain three datasets at the same time — "log," "model history," and "UI state." There is only one; everything else is a projection.
Model-Visible ⟺ Recorded
The architecture doc states this principle as a formula (source: docs/architecture.zh.md · "Session Log"):
The session log is the authoritative basis.
deriveMessages()projects the model history; the rawassistant/chunkevents guarantee replay and UI fidelity. Forking, recovery, transcript rendering, telemetry, and persistence all derive from this event stream.Model-visible ⟺ recorded: the messages entering at
step/startplus the foldedrequest/headercan rebuild every request.
"Model-visible ⟺ recorded" is the heart of this lesson: anything the model can see is necessarily recorded in the log; anything not in the log is invisible to the model. There is no black hole where "the model secretly used something that was never recorded," and no ghost where "the log recorded something the model never saw." Precisely because of this, persistence, recovery, fork, replay, telemetry, and UI — six subsystems that seem completely unrelated — all draw from the same event stream and can never contradict one another.
💡 Analogy: this is not "keeping a diary" but "recording the whole process on video." A diary is written afterward from memory — it can omit and alter things; a video is the raw record from the moment an event happens — every frame is real.
3. Three-Level Structure: Session → Turn → Step
The session log is not a jumble; it has a clear three-level structure:
会话 → 轮次 → 步骤:事件全都追加进日志,从任意检查点都能重建
- Session: an entire interaction history, backed by an append-only event log, with a globally unique
SessionId; - Turn: starts when a message is claimed and ends when the response for it is finished. A turn is bracketed by two events,
turn/startandturn/end;turn/endfaithfully records the reason it ended — normal completion,abortedcancellation,errorfailure, and crash recovery synthesizesinterrupted; - Step: one model request + its tools. A step is bracketed by
step/startandstep/end; every successful model call leaves anassistant/message— even if the call returns empty content or is truncated because ofmax-tokens, the log still records it (empty content simply doesn't enter the derived message history; the persisted event and usage are both there).
The agent-lifecycle doc nails the division of labor in one sentence (source: docs/agent-lifecycle.zh.md):
Durable replay facts are stored in
session/event, while live control and state are stored inagent/*.
In other words: the log handles "what happened," while events handle "how it's going right now." The former can be replayed precisely; the latter drives real-time behavior (such as the running / idle state and the inbox queue).
A single turn often contains several steps: the model says "I want to read a file" → the tool executes → the result returns → the model says "I want to edit a file" → the tool executes → ... until the model decides the task is done and the turn closes. Only closed turns form stable fork / checkpoint boundaries.
4. Recover Everything from the Same Log: Recovery, Fork, and Search
Recovery: Resuming with resumeSessionId
DSH's agent loop driver (dsh-agent-loop) provides two paths into a session:
- Create:
ctx.agents.create(...)— start from scratch with a newsessionId; - Resume:
ctx.agents.resume({ resumeSessionId, ... })— load an already-existing persisted session and keep running.
The agent-loop doc describes recovery this way (source: packages/core/agent-loop/README.zh.md):
ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): loads a persisted session throughctx.sessionPersistence, registers the agent under the same id, rebuilds the history... Turn numbering and derived history continue from the loaded log. This operation requires a session persistence backend; without persistence,resumerejects with a clear error.
Three key points: recovery is not a replay from scratch but a continuation with continued numbering (turn numbering and derived history continue from the loaded log); recovery requires that the session was actually persisted; and if no persistence backend is configured at all, it fails loudly rather than pretending to succeed — better to be unsupported than to hand you a fake recovery that "looks the same but has actually lost context."
Fork: Copy Boundaries
Want to "try a different path"? Use ctx.sessions.fork(source, boundary?, childSessionId?). Its semantics (source: packages/core/session/README.zh.md):
Resolves the live session object or id, takes the seed up to and including the
boundaryevent ordinal (defaulting to the current last event), requires that the selected prefix ends with no open turns, then creates a live child session with lineage metadata.
- Copy boundary: defaults to the current last event, and
boundarycan be specified explicitly, but the selected prefix must end with a closed turn — you can't fork in the middle of a turn; - Lineage metadata: the child session records information such as
parentSession, so it's immediately clear which one is the copy; - Original session untouched: fork is just "read the log + derive a new session"; the source session isn't modified at all.
Search: session-query Full-Text Search
The log grows day by day — how do you precisely find things in it? DSH provides a dedicated family of session search capabilities (session-query). It "provides authorized search over live and persisted session logs, independent of compaction" — even if a piece of context is later replaced by compaction, the raw log remains and is still searchable.
searchSessions(): full-text search across sessions, returning results grouped by the best-matching events;searchEvents(): search events within a single session;- The SQLite provider implements indexing with full-text search (FTS);
- Security detail: query terms are interpreted literally and are never treated as executable search syntax — treat search as data, not as code.
🎁 What the three have in common: recovery, fork, and search all only read the log. Precisely because the log is authoritative, complete, and append-only, these three operations can each stand on their own without conflicting.
Key Points Recap
These five sentences are all you need to remember from this lesson:
- Reconstructible at runtime is DSH's promise: everything the model can see is recorded in the authoritative session log — model-visible ⟺ recorded.
- Session = append-only event stream: events can only be appended, never rewritten; the model message history and the UI transcript are projections derived from the log, and the log is the single source of truth.
- Three-level structure: session → turn → step. A turn claims a message and closes; a step = one model request + its tools.
- Recovery and fork:
resumeSessionIdcontinues with continued numbering from the persisted log;fork(source, boundary)copies a parallel session at the stable boundary of a closed turn, leaving the original session untouched. - Full-text search: session-query makes the log searchable (
searchSessions/searchEvents), independent of compaction — even old context that was replaced can still be found.
🚀 Next lesson preview: memory alone isn't enough for the model — it has to be able to act. In Lesson 4 we'll cover "Tools and Execution" — how the agent turns "calling a function" into a real action that is permissioned, sandboxed, and recorded.
Quiz · Loops and Sessions
Answer each question, then submit to check your result.
