Loading blog posts...
Loading blog posts...
Loading...

Pi Durable can help an agent recover after a redeploy, but saving its conversation won't stop it from sending the same invoice twice.
Quick summary
harness.resume on startup.replay: 'safe' only when repeating them causes no harm.Consider an agent 40 minutes into a maintenance job: it has inspected a repository, recorded a plan, and started a deployment when its container gets replaced. A transcript might show what the agent intended, but recovery also needs to know which work completed, which state changes committed, and whether that deployment happened.
Pi Durable provides persistence and execution machinery for that problem. It shipped alongside Pi 1.0 from the developers of the Pi coding agent, now under Earendil. It's a library you build an application around, with separate building blocks for conversation history, application state, and unfinished work:
| Building block | What it holds or coordinates | Practical purpose |
|---|---|---|
| Harness | Storage, commits, and scheduling | Coordinates execution and recovery |
| Conversation | User messages, assistant replies, and tool results | Preserves the transcript |
| Task | A checkpointed unit of work | Tracks a model turn, tool call, or custom workflow step |
| Document | Typed JSON application state, defined with defineDoc | Stores structured progress outside the transcript |
| Submission | An identifiable piece of input | Lets callers retrieve and await submitted work again after a restart |
A commit can write transcript entries, Document changes, new Tasks, and Submission updates together, so recovery doesn't find a recorded decision without its corresponding application-state update. That guarantee stops at the storage boundary: it cannot make a database commit and an external email delivery happen as one indivisible operation.
Start with the packages named in the official repository:
bashnpm install --save-exact @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
The --save-exact flag records exact direct-dependency versions. Commit both package.json and your lockfile: Pi Durable is experimental, and its API may change. Use the repository's current examples for the complete application; don't guess constructor arguments.
The startup sequence should cover these steps:
openNodeSqliteStorage('./agent.sqlite'), imported from @earendil-works/pi-durable/storage/sqlite/node.Harness.open.harness.resume on every startup before accepting new input.root.submit.Restarting the application should resume persisted work, not submit the original request as a new job.
For a webhook-driven triage agent, pass a stable requestId, such as job-42, with the submission. A retry using that ID returns the existing Submission, according to the Pi Durable documentation and examples. Use the incoming event's stable identifier where possible. Generating a fresh identifier for every delivery attempt defeats deduplication.

An ops agent's tools for reading configuration, checking deployment status, and starting a deployment need different recovery policies.
| Tool action | Replay choice | Reason |
|---|---|---|
| Read a configuration file | Mark replay: 'safe' if repeated reads are harmless | Repeating the read doesn't change the system |
| Query deployment status | Mark replay: 'safe' if the endpoint is read-only | A fresh status check can help recovery |
| Start a deployment | Leave unmarked | The previous call may already have started it |
| Send an invoice | Leave unmarked | Repetition may create a duplicate external action |
With defineTool, Pi Durable makes replay safety an explicit choice. An interrupted tool marked safe can run again; for an unmarked tool, the harness reports the interruption to the model, as described in the official announcement.
That's the right default. A process can die after an external service accepts a request but before the agent records the result, leaving the outcome uncertain.
Warning
A Submission's requestId deduplicates conversation input. It does not make external actions happen exactly once. For payments, emails, or deployments, use the provider's idempotency mechanism or reconcile the external result before retrying.
An idempotency key is a stable identifier that lets a service recognize repeated requests for the same operation. Keep that identifier across retries.
A Document can track completed actions and external operation IDs. It still cannot close the crash window between an external action succeeding and your local completion record being committed; recovery needs a way to query the provider or otherwise reconcile that uncertainty.
For an ops agent, prefer a workflow that starts a deployment, retains its external operation ID, and checks status separately. If interruption leaves the start result unknown, require reconciliation or approval before retrying.
A container that writes ./agent.sqlite into its disposable filesystem loses its durable state when that filesystem disappears. Put the file on a persistent volume before testing recovery.
The official repository recommends SQLite for long-running agents. Choose another backend for a concrete requirement; an embedded database isn't inherently too small.
| Backend | Good fit | Constraint |
|---|---|---|
| Memory | Tests and throwaway runs | State disappears when the process exits |
| SQLite | Long-running agents | Needs persistent storage, backups, and one process owner |
| JSONL | Small histories you want to inspect by hand | Keeps the full history in memory |
| Custom storage | A backend required by your infrastructure | You own implementation and validation |
JSONL stores one JSON record per line. Its readable format is useful, but retaining the history in memory makes it a poor default for an indefinitely growing agent. The library also exposes a small storage interface, conformance tests, and benchmarks for custom implementations. Those help validate an adapter; they don't make a proposed Postgres or key-value backend production-ready automatically.
Keep deployment rules explicit:
Pi Durable won't restart the process itself. On a Linux server, systemd with Restart=always is a straightforward choice; in containers, use a restart policy and a mounted persistent volume. SQLite handles database persistence. Every replacement process still needs to call harness.resume.

An overnight coding agent is a sensible first application because the official repository already includes a small coding-agent example. For structured planning state, follow the vacation-planner example and keep progress in Documents.
The built-in capabilities and extension points cover several needs:
| Need | Relevant API | What you still decide |
|---|---|---|
| Local coding tools | CodingTools and NodeExecutionEnv | Filesystem access, shell permissions, and execution isolation |
| Tool policies | wrapTool | Logging, approval checks, and rate limits |
| Runtime extensions | createRegistry, defineExtension, and loadExtension | Which changes are safe during active work |
| Conversation-specific permissions | thread.configure | Which tools each conversation may access |
| Context management | root.compact | When and how to reduce model context |
| Live progress | thread.watch and thread.watchEvents | UI behavior and reconnect handling |
CodingTools, from @earendil-works/pi-durable/tools, provides read, write, edit, and bash tools. NodeExecutionEnv, from @earendil-works/pi-durable/env/node, runs them locally, so their permissions are permissions granted to your agent.
For an ops conversation, thread.configure({ tools: { remove: [deploy] } }) can remove access to a deployment tool. Put approval and audit behavior in a tool wrapper. Don't rely on a prompt to remember the policy.
Extensions can add tools, hooks, prompt sections through section, wrappers through wrapTool, and custom tasks through defineTask. The documented registry.install(await loadExtension('./ops.ts')) pattern supports loading new extension code while the agent runs, but hot changes deserve recovery tests too.
The official compaction example uses reserveTokens: 16384 and backgroundTokens: 32768; compaction controls what enters the model's context without discarding the durable record.
There are no built-in subagents. The examples show how to implement one as a tool, with api.createTask and api.waitForTask supporting child work.
Test a deployment interruption after the external service accepts the request but before the agent stores its result. That is a better durability test than stopping an idle process and confirming that its messages reappear.
The official examples, under packages/durable/test/examples, are the best starting point because they track the API more closely than a blog post can. Use them to test your chosen workflow, then add failures around its external effects.
Pi Durable fits long-running coding jobs, structured planners, webhook-driven triage, and ops workflows with explicit recovery policies. Skip it for one-shot scripts, stateless chat, or a team that requires a stable API this quarter.
You still own model credentials, tools, the UI, storage backups and cleanup, process supervision, and external-action safeguards. The docs also describe adapting storage for Bun or a Cloudflare Durable Object; those integrations require setup.

--save-exact, then commit package.json and the lockfile.requestId and confirm that it resolves to the existing Submission.harness.resume recovers work from the persistent database.packages/durable/test/examples.Pi Durable makes interrupted work recoverable, but your tool contracts determine whether recovery is safe. Start with one bounded agent, persistent SQLite storage, and a deliberate crash test around its riskiest external action; expand its autonomy only when that test produces an explainable result.