Engineering

How Pi Pocket is built.

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.

TypeScript, no build Preact and htm SQLite through Pi Durable Node.js 22.19+
Architecture

The parts.

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.

Browsersweb/
Preact + htmservice workerpushshare targethome-screen app
JSON API · server-sent events · long polling
HTTP serverhttp.ts
/api routesevent streamuploadssandboxed artifactsbrowser framesinvites
calls
PocketAppapp.ts
roomscommandscollabalertsspendschedulesgoalschangesworktrees
commits · commit listener
Pi Durable harness@earendil-works/pi-durable
conversationstasksdocumentsextensionstask graph
one file
SQLite~/.pi-pocket/pocket.sqlite
Launchersrc/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.

Extensionssrc/server/extensions/

Tools, hooks, prompt sections, and tasks, installed into the harness and reinstalled when edited. The owner's drop-ins load after them.

Chromiumbrowser.ts

One 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~/.pi/agent/

Sign-ins, settings, skills, and prompt templates, shared with the Pi command line, and Lancet Guard when it is installed.

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.

commands.ts

Commands

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

One commit listener

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

Event stream

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

Live reload

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

Small updates

Long tool output and file contents are clipped in the live view. A tab asks for the full entry when someone expands it.

Request path

A message, start to finish.

What happens between tapping send on a phone and every open tab showing Pi's answer.

  1. web/composer.js

    Sent with an id

    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.

  2. commands.ts

    Checked, then queued

    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.

  3. pi-durable

    Run as durable tasks

    The model request and each tool call are tasks that commit as they go. Plan mode, Lancet Guard, and goals hook into those tasks.

  4. app.ts

    Heard once

    The commit listener sees each commit, records the author, marks the session busy, counts spend, and tells the rooms watching that conversation.

  5. room.ts

    Sent as changes

    The room waits up to 90 ms to batch changes, then sends each tab what it is missing over its event stream.

  6. web/store.js

    Rendered

    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.

Durability

Rules that survive a crash.

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

Shown once stored

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

Sent once

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

Asked once

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"

Rerun only when harmless

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

Time that survives

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

Forks are data

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.

State

Where everything lives.

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.

DocumentHoldsA fork
pocket.sessionsThe session list: title, folder, who started it, where it was forked from, spend limit, worktree. One for the server.adds a session
pocket.chatThe side chat and activity linesstarts its own
pocket.authorsWho wrote each message to Pi, and whose work the ones sent for people arecopies, to the fork point
pocket.pinsPinned messagescopies, to the fork point
pocket.reactionsEmoji reactionscopies
pocket.notesThe shared notes pagecopies
pocket.planPlan modecopies
pocket.decisionsWho allowed or denied each guarded callcopies
pocket.artifactsArtifacts, with each version's body in pocket.artifact-bodycopies
pocket.codemode-storeValues scripts keep between runscopies
pocket.browserThe page the browser last showed, and its size, to open it again after a restartcopies
pocket.turnsTake turns: who drives, who askedstarts empty
pocket.subagentsNamed subagents and the tasks reporting their answersstarts empty
pocket.schedulesScheduled messagesstarts empty
pocket.goal"Done when": the check and its triesstarts empty
pocket.spendSpend per person, and what was counted. One for the server.—

Outside the database

config.json people, roles, hashed tokens, settings push.json VAPID keys, push subscriptions uploads/ files sent to Pi worktrees/ sessions' git worktrees extensions/ the owner's drop-ins browser/ the built-in browser's profile

All of it is in ~/.pi-pocket/, or wherever PI_POCKET_DIR points. The config files are written atomically with mode 0600.

Extensions

Features are extensions.

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.

  1. prompt.tssection

    The system prompt: guidelines for a phone-sized screen, the project's AGENTS.md files, Pi's skills, and the working directory. Always on.

  2. artifacts.tstool

    Versioned HTML, Markdown, and SVG artifacts, opened in a sandbox.

  3. browser.tstool · section

    A 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.

  4. subagents.tstool · tasks

    Background subagents. An anchor task owns each one; a reporter task carries each answer back to the parent.

  5. schedules.tstool · task

    Scheduled messages, and Pi's tool for scheduling its own follow-ups.

  6. goals.tshook · section

    After each final answer, runs the session's check and keeps Pi going while it fails.

  7. plan.tshook · section

    Plan mode: calls that would change something are blocked before they run.

  8. guard.tshook

    Lancet Guard: risky bash, write, and edit calls wait for a person. Off by default.

  9. codemode.tstool

    Scripts 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.

Security

Narrow ways in.

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.

  • Sign-in. Random tokens, stored hashed, in an HttpOnly, SameSite=Lax cookie. Invites are one-time links that expire after 15 minutes.
  • Requests. Anything that changes something must carry an X-Pocket header, which a form on another site cannot send.
  • Pages. The app loads nothing from other sites: its policy allows only its own files and its hashed inline scripts. Replies are sanitized, and artifacts run in a sandbox with no access to the app.
  • Browser. Pages run in Chromium on the server and reach people only as images, so they never touch the app. Using one takes steering rights, files are the owner's, and each address someone opens shows in the chat.
  • Roles. Owner, steer, or view, in every session or only one. Lancet Guard approvals can require someone other than whoever asked.
!

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.

Practices

Built to be changed.

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

Nothing to compile

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

Few 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

Tested on the real core

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

Type-checked

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

Safe to edit live

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

Written to be read

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.