- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
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.