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.
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 } })
}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.
Each run gets two files in /tmp/tanstack-runs:
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.
To read a journal yourself, handle stalled reads, seed sentinels, or replay against a stored log, see Run Journal Internals.