room.ts
Rooms
A room per conversation someone is watching. It batches changes for 90 ms, turns entries into compact JSON, and sends each tab only the entries and fields it does not have yet.
One Node.js process, one SQLite file, and a web app with no build step. Pi Durable runs the agent and commits every step before anyone sees it. Pi Pocket adds the people, the phone, and the ways in. This page covers the parts, how a message moves through them, and the rules that keep them consistent.
Browsers talk to one server over a JSON API and an event stream. The server keeps its state in Pi Durable's storage and rebuilds the rest from it when it starts. Nothing a browser sees exists only in memory, except who is here and who is typing.
web/http.tsapp.ts@earendil-works/pi-durable~/.pi-pocket/pocket.sqlitesrc/launcher/Runs the server as a child and restarts it when the app asks (exit code 75) or after a crash. Keeps the tunnel up: this device, LAN, Cloudflare, or Tailscale.
src/server/extensions/Tools, hooks, prompt sections, and tasks, installed into the harness and reinstalled when edited. The owner's drop-ins load after them.
browser.tsOne headless browser driven over the DevTools protocol through a pipe, with a page per session that Pi's browser tool and the Browser panel share. Started on first use.
~/.pi/agent/Sign-ins, settings, skills, and prompt templates, shared with the Pi command line, and Lancet Guard when it is installed.
room.ts
A room per conversation someone is watching. It batches changes for 90 ms, turns entries into compact JSON, and sends each tab only the entries and fields it does not have yet.
commands.ts
Everything people ask of Pi checks role, invite scope, and take turns first. Changes others should know about become activity lines in the session's chat.
app.ts
The one place that hears every commit: it records authors, tracks busy sessions, counts spend, and updates rooms. Features react to commits instead of polling.
store.js
Server-sent events by default. When a tunnel holds the stream back, the app switches to long polling and remembers that for a day.
reload.ts
A changed extension is imported again and installed under the same name. A tool call already running finishes on the old code; a module that fails to load keeps its last good version.
projection.ts
Long tool output and file contents are clipped in the live view. A tab asks for the full entry when someone expands it.
What happens between tapping send on a phone and every open tab showing Pi's answer.
web/composer.js
The tab posts the message to /api/c/:id/submit with an id of its own, so sending again after a network error does not send twice.
commands.ts
Role, invite scope, take turns, and spend limits are checked, and a prompt template expands. The message goes into the conversation's inbox with the request id u:<person>:<id>, which also says who wrote it. While Pi works, it joins the run or waits as a follow-up.
pi-durable
The model request and each tool call are tasks that commit as they go. Plan mode, Lancet Guard, and goals hook into those tasks.
app.ts
The commit listener sees each commit, records the author, marks the session busy, counts spend, and tells the rooms watching that conversation.
room.ts
The room waits up to 90 ms to batch changes, then sends each tab what it is missing over its event stream.
web/store.js
The store applies the update and Preact renders it. A tab that reconnects gets the whole view again, so every device shows the same thing.
Pi Durable provides tasks, checkpoints, and atomic commits. Pi Pocket uses them so that a restart at any moment changes nothing anyone can see.
commit, then show
Browsers render committed state only. Pi Pocket's documents change in the same commits as the transcript, so the two never disagree after a crash.
request ids
Every message to Pi has a request id, which also says whose work it is. A retried send, a resend, or a scheduled message sent again after a restart finds the one already there.
memos
A hook keeps its result in its task's memo: an approval's answer, a goal's check. After a restart the hook runs again and reads the memo instead of asking or checking again.
replay: "safe"
A tool call cut off by a restart runs again only if running it twice is harmless. Otherwise the model is told it was interrupted and checks what happened.
durable tasks
A scheduled message is a task that sleeps in the harness. A restart while it sleeps keeps it sleeping; one after its time sends it at once, and only once.
fork policies
Each document says whether a fork copies it or starts empty. A fork's setup trims what it copied, such as pins and authors, to the fork point in the same commit.
Pi Pocket's documents sit beside Pi Durable's own (the transcript, the agent's settings, the inbox, the live run, usage) in pocket.sqlite. Their kinds are stored data, so they are never renamed.
| Document | Holds | A fork |
|---|---|---|
pocket.sessions | The session list: title, folder, who started it, where it was forked from, spend limit, worktree. One for the server. | adds a session |
pocket.chat | The side chat and activity lines | starts its own |
pocket.authors | Who wrote each message to Pi, and whose work the ones sent for people are | copies, to the fork point |
pocket.pins | Pinned messages | copies, to the fork point |
pocket.reactions | Emoji reactions | copies |
pocket.notes | The shared notes page | copies |
pocket.plan | Plan mode | copies |
pocket.decisions | Who allowed or denied each guarded call | copies |
pocket.artifacts | Artifacts, with each version's body in pocket.artifact-body | copies |
pocket.codemode-store | Values scripts keep between runs | copies |
pocket.browser | The page the browser last showed, and its size, to open it again after a restart | copies |
pocket.turns | Take turns: who drives, who asked | starts empty |
pocket.subagents | Named subagents and the tasks reporting their answers | starts empty |
pocket.schedules | Scheduled messages | starts empty |
pocket.goal | "Done when": the check and its tries | starts empty |
pocket.spend | Spend per person, and what was counted. One for the server. | — |
All of it is in ~/.pi-pocket/, or wherever PI_POCKET_DIR points. The config files are written atomically with mode 0600.
Much of what Pi Pocket adds around the agent is Pi Durable extensions: tools, hooks, prompt sections, and task definitions, installed in this order. Editing one reinstalls it in the running server.
prompt.tssectionThe system prompt: guidelines for a phone-sized screen, the project's AGENTS.md files, Pi's skills, and the working directory. Always on.
artifacts.tstoolVersioned HTML, Markdown, and SVG artifacts, opened in a sandbox.
browser.tstool · sectionA real browser Pi shares with the people in a session: snapshots with element refs, clicks, typing, screenshots, the console. In plan mode it may only look.
subagents.tstool · tasksBackground subagents. An anchor task owns each one; a reporter task carries each answer back to the parent.
schedules.tstool · taskScheduled messages, and Pi's tool for scheduling its own follow-ups.
goals.tshook · sectionAfter each final answer, runs the session's check and keeps Pi going while it fails.
plan.tshook · sectionPlan mode: calls that would change something are blocked before they run.
guard.tshookLancet Guard: risky bash, write, and edit calls wait for a person. Off by default.
codemode.tstoolScripts that call the other tools. Each call goes through the same hooks as a direct one.
The owner's own modules in ~/.pi-pocket/extensions/ load after these, stay off until turned on, and import the same packages through a node_modules link.
Pi runs commands as the person who started the server, so the app limits who can ask it to, and what a page or a reply can do.
X-Pocket header, which a form on another site cannot send.Steering means shell access. Anyone who can steer can make Pi run commands on the machine. Worktrees keep a session's files apart, and spend limits stop runs, but neither is a sandbox. SECURITY.md spells out what is protected.
Pi Pocket is often edited by Pi from inside itself, so its code has to stay easy to read and safe to change while it runs.
no build
Node runs the server's TypeScript by stripping its types, so only erasable syntax is used. The web app is plain ES modules with Preact and htm.
dependencies
Earendil's Pi packages, pinned to the versions tested, plus Preact, htm, marked, DOMPurify, qrcode, undici, and jiti. Web Push is built in.
npm test
Tests run the real server against a scripted model: real SQLite, real git repositories, and restarts by reopening the same data folder, with the clock moved forward for schedules.
npm run check
The TypeScript compiler checks the server and the tests. Web files get Node's syntax check after every edit, since a broken one blanks every open tab.
AGENTS.md
Web files reload every browser, extensions reinstall in place, and anything else waits for a restart from the menu, after which running work resumes.
docs/map.md
Each module starts with what it owns, comments say why in plain sentences, and a short map of the code helps agents find their way.