Pi Durable is the agent harness underneath every pi-daemon session. It writes conversations, model steps, tool calls and your own state to storage in atomic commits before anything is shown, and a process that reopens the storage carries on where the last one stopped. pi-daemon adds no bookkeeping of its own on top: everything a client sees is derived from what Durable committed.
01The model
A session is one store of conversations, documents and tasks
pi-daemon opens one Durable Harness per session over that session’s own SQLite file. Everything in it changes through one line of atomic commits: a commit can append transcript entries, edit documents and create tasks together, and either all of it is stored or none of it is.
Fig. 2.1The documents with a pi-daemon. prefix are pi-daemon’s; everything else is Durable’s own. All of it is in the one file.
Durable term
What it is
In pi-daemon
conversation
a transcript of immutable entries: pi.user, pi.assistant, pi.tool-result, pi.system, pi.reset, pi.compaction
an AHP chat; the root conversation is the session’s default chat ahp-chat:/<id>/1
document
typed JSON committed with the entries, per conversation or per task
model and thinking, the inbox, usage; plus questions, approvals and changeset baselines
task
a durable state machine with a checkpoint per step and an owner
each model request and each tool call; subagents and compaction
submission
input or an entry handed to a conversation, which you can wait for and retry by request id
every AHP turn, steer and queued message, ACP prompt and Chord submit
extension
a named bundle of tools, prompt sections, hooks, wrappers and tasks in a registry
the profile’s built-in tools, pi-prompt, MCP, ask_user, subagent, approvals and the profile’s own modules
02One answer
What one answered input writes
Fig. 2.2A turn is one model response and its tool calls; a run is the turns from an input to its answer. The conversation is busy for the whole run.
The system prompt is stored as positional pi.system entries, and only what changed is sent again, so provider prompt caches stay warm. That is why pi-daemon builds each worker’s prompt sections once per directory and keeps them stable between turns.
03Commit before show
Streaming without losing the stream
A model’s partial answer and a tool’s running output are committed as they arrive, at most every 100 ms. A crash can therefore lose at most that window of streamed text, never an entry. Everything a viewer sees comes from committed state:
Views.viewState() and watch() give a conversation’s structural view and the exact Chord operations of each commit. A slow watcher holds at most 100 frames, then gets one frame with the newest view.
Agent events.watchEvents() derives coding-agent style events from commits: message and tool updates as deltas. A consumer more than 100 batches behind gets a fresh snapshot.
Late joiners. A client that attaches mid-run starts from the current view; nothing is replayed.
pi-daemon’s AHP turn stream is built from watchEvents(), /chord exposes watch() directly, and an AHP client that subscribes mid-turn gets the turn as the snapshot’s activeTurn, then the rest live.
04Exactly once
A request id is the whole idempotency story
Durable submissions take a requestId. Submitting again with the same id returns the existing submission instead of a second one, and that id is persisted in the store, so it holds across restarts. pi-daemon passes every client’s identity straight through rather than inventing receipts:
From
Durable request id
AHP turn
ahp:<clientId>:<turnId>
AHP steering or queued message
ahp:<clientId>:<messageId>
ACP prompt
_meta["pi-daemon/requestId"]
Chord submit
the call’s requestId
Fig. 2.3The server resends a submission at most three times after its worker dies; the request id makes each resend the same submission.
Keep the client id
The guarantee is per client identity. An application that restarts must reconnect with the same AHP client id for a resent turn to be recognised.
05Tasks
Work resumes from its last checkpoint
Fig. 2.4Running work picks up where it stopped. A tool call is not repeated unless its tool says that is safe.
Tool
After a crash mid-call
read, write, edit, bash
not rerun: the model gets interrupted with the output committed so far
MCP tools mcp__<server>__<tool>
not rerun: interrupted
ask_user
replay-safe: the same question, still waiting for its answer
subagent
replay-safe: finds the same child conversation and submission
tool_search
replay-safe: loads the same matches
Ownership makes this composable. A task can own child tasks and conversations: it waits on them, aborting it aborts them first, bottom-up, and it is not terminal until they are. A task marked background is a boundary that survives its parent’s abort.
06Busy
Steer, follow up, write, abort
Fig. 2.5Submitting to a busy conversation queues the submission in its pi.inbox document; when it is placed decides what it does.
The profile’s settings.json chooses steeringMode and followUpMode exactly as for TUI Pi: one queued item per turn, or all of them at once. If a run fails, queued items stay in the inbox until the next submission places them, oldest first.
07Forks and subagents
More conversations, same store
Fig. 2.6Forks, side chats and subagents are conversations of the session, so they share its store, its worker and its fate.
Because Durable history is append-only, pi-daemon refuses AHP’s chat/truncated: fork the chat instead. Deleting a chat removes it from the session but keeps its history in the store; deleting the session deletes the file.
08Compaction
Long conversations keep working
Compaction summarises older entries into a pi.compaction entry that heads what the model sees next; nothing is deleted. It runs in the background as context fills, before a request that would overflow, and once more after a provider rejects a request as too long. The profile’s settings.json drives it as it drives TUI Pi.
pi-daemon reports how each one ends rather than letting a failure pass silently: compacted, skipped, aborted or failed, on the compaction_end event and as the answer to a manual /compact or Chord conversation.compact. A turn whose automatic compaction fails still runs on the full history, and the next turn tries again.
09What pi-daemon builds
Every feature is a Durable primitive
pi-daemon feature
Built on
resume after restart
live tasks and unsettled submissions in committed state, reported to the catalog as liveWork before the commit that starts work and after the one that ends it
agent questions
the ask_user tool and a task-scoped pi-daemon.input document: the question outlives restarts and retires with the call
tool approvals
beforeTool and afterTool hooks holding the call, and a task-scoped pi-daemon.approval decision: a waiting call keeps waiting across restarts
changesets
a conversation-scoped pi-daemon.changes document of file baselines, recorded before a chat’s write or edit first touches a file
per-session tools
Durable’s env: an execution environment per call in the session’s cwd, with the profile’s and the session’s variables
profiles
a registry per worker holding exactly the profile’s built-ins and modules; settings as live getters over settings.json
turn streaming
watchEvents() projected into AHP chat/* actions and ACP session updates
native views
viewState(), watch() and watchTaskGraph() served over /chord
10Limits
What the guarantees do not cover
Power lossSQLite runs in WAL mode with synchronous = NORMAL: commits survive a process crash, but the newest one may be lost if the machine itself fails.
One owner per storeDurable has no cross-process locking. pi-daemon routes each session to exactly one worker, and never shares a store with anything else.
Live processesTerminals and stdio MCP servers are processes, not Durable state. A restart ends terminals; MCP servers start again with the session.
In-memory viewsThe AHP replay window and connections are memory. After a restart, clients get fresh snapshots, which AHP explicitly allows.