Case study · Developer tool · AI agents · 2026
Pico
as state machines.
A browser workspace for durable coding-agent sessions. The agent keeps its work when the stream drops, the tab sleeps, or the server restarts.
The problem
A coding agent can work for many minutes. During that time, the phone that shows it can lock, the Wi-Fi can drop, and the tab can reload. The server can crash, or restart after each rebuild.
After each of these events, the screen must show the true state of the session. And an agent that already sent a notification, started a job, or started a child session must not do it again.
Ali's role
Ali made Pico: the product concept, the interface design, the server, the web client, and the local-first architecture.
Pico runs its agents on the open-source pi-durable runtime (@earendil-works/pi-durable). The runtime stores each step of a run in SQLite and continues unfinished runs when it opens again. Pico adds the live event stream, the reconnect logic in the browser, and its own agent tools, which follow the replay rules of the runtime.
The state machines
Each diagram comes from the same data as the tables under it. The tables give the same facts as text.
Machine 01 · 6 states · 12 transitions
The live event stream
The browser gets all live updates through one Server-Sent Events stream. Each event has an ID. The first message on a new stream is always a full baseline of the session. After that, the server sends only patches against what the browser already has.
States
- Connecting
- The browser opens the stream. It sends the last event ID that it saw, from memory or from session storage.
- Replaying
- The server sends the events that the browser missed, from a window of the last 512 events. It skips old state events, because the baseline replaces them.
- Baseline
- The server sends the full state of the selected session and the session list. Events that arrive during this step wait in a queue.
- Live
- The server sends each new state as a patch: the start, the delete count, and the new items. Streamed text arrives as deltas.
- Checking
- The stream failed. The browser asks the health endpoint whether the server is up.
- Unreachable
- The server does not answer. The page shows one “server unreachable” view, not one error for each request.
Transitions
| From | Event | To |
|---|---|---|
| Start | page opens | Connecting |
| Connecting | stream opens with a last event ID | Replaying |
| Connecting | stream opens without a last event ID | Baseline |
| Replaying | missed events sent | Baseline |
| Replaying | ID too old, resync notice | Baseline |
| Baseline | queued events delivered | Live |
| Live | patch or delta | Live |
| Live | stream error | Checking |
| Live | tab back after 30 s, stream closed | Connecting |
| Checking | server answers | Connecting |
| Checking | no answer | Unreachable |
| Unreachable | retry timer (1, 2, 3, 5 s) | Checking |
Machine 02 · 6 states · 9 transitions
One tool call across a crash
Each tool call is a durable task. The runtime stores the intent of the call before the tool runs. If the process dies during the call, the next process decides what to do from the replay policy of the tool. Pico marks its tools with external effects as replay-safe, and each of them checks a stored memo before it acts.
States
- Intent stored
- The runtime stores the call before the tool runs.
- Running
- The tool does its work.
- Result stored
- The result goes into the transcript.
- Cut off
- The process crashed or restarted during the call.
- Interrupted
- The tool is not replay-safe. The model gets an “interrupted” error with the output so far, and decides what to do.
- Rerun
- The tool is replay-safe. It runs again from the top and first reads its memo.
Transitions
| From | Event | To |
|---|---|---|
| Start | model calls a tool | Intent stored |
| Intent stored | execute | Running |
| Running | tool returns | Result stored |
| Running | crash or restart | Cut off |
| Cut off | reopen, tool is not replay-safe | Interrupted |
| Cut off | reopen, tool is replay-safe | Rerun |
| Rerun | memo found, return the earlier result | Result stored |
| Rerun | no memo, do the work | Running |
| Interrupted | error result stored | Result stored |
What can go wrong
Failure 01
The live stream drops
A phone locks, the network changes, or a proxy returns a 502. Events that the server sends during the gap do not arrive.
How the design handles it
- Every event has an ID, and the browser keeps the last one in session storage. A new stream sends it back, and the server replays the missed events from a window of 512.
- Event IDs start from the clock when the server starts, so they keep growing across restarts. An ID from before a restart gets no replay; the baseline covers it.
- When the window no longer covers the ID, the server says so in a resync notice and sends the full baseline. The browser never applies a patch to the wrong base.
- After the baseline, state changes go out as small splices: the server finds the common start and end of the item list and sends only the middle.
- The replay window keeps only the newest state event of each session, so old full states do not push out the small events.
Failure 02
The server is down
Every request fails at the same time. Without care, the page shows one error toast for each failed request.
How the design handles it
- One small store holds the connection state: unknown, online, or unreachable. A failed request or stream asks the health endpoint once.
- While the server is down, the store checks again after 1 s, 2 s, 3 s, and then every 5 s. The page shows one “server unreachable” view.
- When the server answers again, the page opens a new stream with the saved event ID and loads its data again.
- When a tab comes back after 30 s or more in the background and its stream is not open, it reconnects at once.
Failure 03
A run is cut off by a crash or restart
The agent is in the middle of a run when the process dies. The work must not be lost, and it must not start from zero.
How the design handles it
- Sessions live in one SQLite database. The pi-durable runtime stores a checkpoint at every step of a run and continues unfinished runs when Pico opens again.
- Pico keeps tool state in documents, not in server memory, so a restart does not lose a todo list, a timer, a question, or a child-session link.
- A timer that was due while the server was down fires one time at startup. Its follow-up message has a fixed request ID per run, so a second delivery is refused.
- Terminals and background jobs run in a separate pico-pty-host process. After a server restart, the server takes over the jobs that still run and records the ones that ended. A job with no process is marked “lost” and reported, not shown as finished.
- apps/pico/server/src/core/runtime.ts(opens in a new tab)
- apps/pico/server/src/extensions/schedule.ts(opens in a new tab)
- apps/pico/server/src/extensions/jobs.ts(opens in a new tab)
- apps/pico/server/test/extensions-durable.test.ts(opens in a new tab)
- apps/pico/server/test/pty-host.test.ts(opens in a new tab)
- docs/plans/durable-tools.md(opens in a new tab)
Failure 04
A tool with side effects runs twice
A replay-safe tool reruns after a crash. Without a guard, it sends a second notification, starts a second job, or makes a second child session.
How the design handles it
- Each tool with an external effect checks a memo first. If the memo has a result, the tool returns it and does nothing else.
- A rerun of job_start reports the job that it already started. It never starts the command again.
- A rerun of spawn_session finds the child that the same tool call made. The first message to the child has the request ID spawn:<call ID>, and the runtime returns the existing submission for a request ID that it already has.
- One gap stays open: the effect and its memo are two steps. A crash between them can repeat the effect. Where the effect is a message to a session, the fixed request ID closes the gap.
Numbers
Only numbers from the code or from a count. Each one says where it comes from.
- events in the replay window
- 512
- MAX_REPLAY_EVENTS in apps/pico/server/src/events.ts.
- between health checks while the server is down
- 1, 2, 3, 5 s
- RETRY_DELAYS_MS in apps/pico/src/lib/pico/server-connection.ts.
- in the background before a tab reconnects
- 30 s
- RESUME_RECONNECT_AFTER_MS in apps/pico/src/features/pico/use-app-shell-session-sync.ts.
- server test cases in 26 files
- 121
- Counted test( declarations in apps/pico/server/test with grep, at commit abae556, on 27 October 2026. Counted, not run.
Sources
Paths are in the public alivault/pico repository at commit abae556. Every claim on this page comes from these files.
- apps/pico/server/src/api.ts(opens in a new tab)
- apps/pico/server/src/core/runtime.ts(opens in a new tab)
- apps/pico/server/src/events.ts(opens in a new tab)
- apps/pico/server/src/extensions/jobs.ts(opens in a new tab)
- apps/pico/server/src/extensions/notify.ts(opens in a new tab)
- apps/pico/server/src/extensions/schedule.ts(opens in a new tab)
- apps/pico/server/src/extensions/sessions.ts(opens in a new tab)
- apps/pico/server/test/core.test.ts(opens in a new tab)
- apps/pico/server/test/extensions-durable.test.ts(opens in a new tab)
- apps/pico/server/test/pty-host.test.ts(opens in a new tab)
- apps/pico/src/features/pico/app-shell-utils.ts(opens in a new tab)
- apps/pico/src/features/pico/use-app-shell-session-sync.ts(opens in a new tab)
- apps/pico/src/lib/pico/server-connection.ts(opens in a new tab)
- docs/plans/durable-tools.md(opens in a new tab)
- @earendil-works/pi-durable · README(opens in a new tab)
Next case study
Whimcall