Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- Providers
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
The Run Journal (Advanced)
The run journal writes each agent event to disk. A new server can read that log after the process that held the pipe exits.
Any server with the sandbox handle and the run id can read the whole run again, from the first byte.
Turn on the journal#
- Pass both runs and durability to withSandbox. With only one of them, or neither, there is no journal and nothing warns you.
- Forward the runId from the request to chat(). The journal path comes from it. Without it, a non-durable run gets a random id that no other server can compute.
- Give the same durability adapter to the middleware and to the response.
import {
chat,
chatParamsFromRequest,
memoryStream,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { claudeCodeText } from '@tanstack/ai-claude-code'
import { memoryPersistence } from '@tanstack/ai-persistence'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
// Local dev only. In production, every replica must reach the same stores.
// See ./takeover for the production wiring.
const { runs } = memoryPersistence().stores
const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: [
'npm install -g @anthropic-ai/claude-code --include=optional && claude --version',
],
secrets: createSecrets({
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '',
}),
}),
})
export async function POST(request: Request) {
const { messages, threadId, runId } = await chatParamsFromRequest(request)
// One adapter for the middleware and the response, so the journal and the
// delivery log describe the same run.
const adapter = memoryStream(request)
const stream = chat({
adapter: claudeCodeText('claude-opus-4-8'),
messages,
threadId,
// Required. A durable run without it throws DurableRunIdRequiredError.
runId,
// Both options, or there is no journal.
middleware: [withSandbox(sandbox, { runs, durability: { adapter } })],
})
return toServerSentEventsResponse(stream, { durability: { adapter } })
}- On the client, add nothing. useChat sends a new runId for every run, and chatParamsFromRequest reads it.
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat() {
const { messages, sendMessage } = useChat({
connection: fetchServerSentEvents('/api/chat'),
threadId: 'support-chat',
})
return (
<button onClick={() => sendMessage('Refactor the auth module')}>
Send ({messages.length} messages)
</button>
)
}Send a message. While the agent works, its output is in /tmp/tanstack-runs/<runId>.ndjson inside the sandbox.
Warning
Never reuse a runId. The journal is append-only, so a reused id appends to the old run's file. The second run then looks empty, or looks like it failed with the old exit code. Nothing throws. See A runId must be unique per run.
How it works#
Each run gets two files in /tmp/tanstack-runs:
- <runId>.ndjson: every NDJSON event the agent prints.
- <runId>.err: the agent's stderr, kept separate so a CLI banner never mixes into the events.
The agent writes to a file, not a pipe. So when your server goes away, no reader disappears, and nothing sends the agent a SIGPIPE. The agent keeps working.
When the agent exits, the shell appends one exit line (the sentinel) to the journal. A reader that sees the sentinel knows the run is over, and it deletes both files.
Every read starts at a byte offset. A fresh read starts at 0. A read on another server, five minutes later, is the same call. Takeover & Detached Runs builds on this. A new server reads the journal from byte 0, and it streams only what the client has not seen.
Go deeper#
To read a journal yourself, handle stalled reads, seed sentinels, or replay against a stored log, see Run Journal Internals.