The launcher signs in to Radius itself and reaches its server as a Radius
client; the brain answers client channels with loopback requests as the
relay owner. Temporary: one Radius connection per request, bundle check
every 10 s; docs/p2-notes.md describes the single-connection follow-up.
p2 rewritten by the Chord v2 authoring rules (decision "App authoring rules confirmed
by the p2 audit"), in four parts merged together:
- core: session lifecycle (relocate, clone, fork, archive) and search rebuild are
persisted jobs on a new server store; Sessions v6 with client-chosen ids and visible
operations; Retry answers with the worker's real outcome; workers push branch,
subagent and status state to the server; roots take the method's ctx.
- services: models, envs, ports, login, mcp, logs, sharing and voice follow external
state (self.follow, followNative) instead of copy loops; methods answer after their
state commits; presence from host.references.
- chat: chat mode, artifacts and prompt resources follow Durable and native state;
shell commands are Chord jobs (shellId is the requestId); status pushed before
send and stop answer; race-free draft claim.
- ui: optimistic updates on web (useOptimistic) and terminal (TuiScope.optimistic,
extended in Chord with a reducer and a pending flag), pending states for long work,
visible rollback, load waterfalls removed; merged with the one composer and
immediate first send (ccff9876e) and the folder + button (5a31a6d5f).
docs/optimistic-ui.md describes the pattern.
Every touch put the transparent input field under the finger, so a long press on
output selected in the empty field (Paste only) and never a word of the output;
the field's focus also opened the keyboard, whose resize redrew all rows. Rows
were replaced whenever output changed them, which dropped any selection.
Now a long press on a character of the output is Chrome's own (word, handles,
Copy); only a blank cell gets the field for Paste. Touch contextmenus are never
cancelled. While output is selected rows are not redrawn (15 s after the last
change, or until the selection ends or the user types, pastes or scrolls). Copy
joins soft-wrapped rows (new Frame row flag from libghostty) and drops the
blanks that fill rows.
- New session: Send clears the composer and shows the message at once, online or not
(plugins/chat/session-start.ts); the session is created when connected and its
messages move to the main conversation's outbox. Refused creations go back to the
composer; connection failures are retried.
- One composer: Main keeps the chat frame (.chat and its dock) while screens change;
screens publish what the dock shows (useChatScreen) and portal their transcript into
it. The composer keeps its focus and the phone's keyboard when the session opens.
- The composer stays in place: the new-session screen shows the session's empty
context bar, and a message sent to an idle conversation stays a message until its
entry shows instead of flashing as a queued row.
- Slots start with the registry's known table (knownRegistry), so a screen inside
slot wrappers mounts once instead of three times.
- A message sent before a conversation has loaded shows alone until it loads.
The worker registers pi-ai's lazy OAuth and Bedrock loaders, so they are chunks of the
snapshot instead of pi-ai source files loaded at run time. The worker entry imports
chord/workers/child and host-packages.ts instead of plugins.ts: a fork loads no esbuild,
no server API and no provider SDK (690 to 301 modules on the phone).
The launcher printed one message for both a heap overrun and an uncaught exception. It now says which, and prints the sanitized, bounded stack of an uncaught exception or a throwing message handler after restoring the terminal. The TUI bundle keeps names so frames are readable.
- P2Tool: origin (any|owner) and scope (any|main|subagent) on the tool; the worker's
Durable registry checks them before each call (guardTool), no tool checks itself.
owner: location tools, reload_plugins, conversations, ports, MCP tools.
main: subagent, rename_session, conversations, ports, reload_plugins. subagent: team.
- originOf reads pi.live.run.inputs through tx.submission: the owner's only if every
input of the run is. Agent-written inputs record the writing turn's origin: subagent
tasks and one-shot prompts, team messages, reports (answerOrigin), peer messages.
No peer origin any more.
- Subagents and one-shot agents get their caller's tools (default all, tools narrows),
except scope main; with codemode the script-only tools; subagents get team. One-shot
agents keep codemode and agent. A send takes away what the caller can no longer give.
codemode is choosable for subagents and in their default tools while the
codemode setting is on; one-shot agents never get it. A subagent with code
mode gets the script-only tools (MCP, terminal, agent, models.*) by name in
its Durable tools array, at spawn/configure and from the worker when the
registry's script-only tools change.
The agent drives the session's terminals (shared with the owner's panel) with
tools.terminal({ command, args }): new-window, list-windows, send-keys, read,
capture-pane, kill-window, with waits (quiet, exit, /regex/) and plain-text
output. The terminal host keeps each terminal's last MiB of output and the
agent's read cursor, answers calls with ids, and does keyed calls once, so the
tool is replay-safe. AgentTerminals serves the session's current worker on its
environments; archiving closes a session's terminals. Without code mode the
worker registry installs tool_search once for all deferred tools.
The artifact tool takes inline: true to also show the produced version below
its transcript card: Markdown/text rendered, HTML/SVG in the sandboxed preview
frame. Drawings fill the frame's width (an SVG with a viewBox or an absolute
size scales to it; an HTML page that is only an SVG or image too) and the frame
takes the content height the preview reports, capped at 70vh. Every viewer sees
it, guests included (owner decision). The TUI renders inline Markdown and text.
SVG previews now carry the diagnostics bridge in CDATA (its && broke the XML).
HTTP servers without an Authorization header sign in with OAuth: stored
tokens in mcp-auth.json, refreshed without a browser. /mcp/sign-in/<server>
returns to a loopback callback when the browser runs on the p2 host (the
device owner, or a relayed/public owner whose browser reaches a nonce probe
on 127.0.0.1), else to /mcp/callback on the origin p2 is reached through.
Pasting the final redirect URL always completes a waiting sign-in.
Web: an MCP sheet (status, restart, sign in/out, paste). TUI: /mcp and
/mcp paste. The sidebar footer's owner entries move into a More menu.
- Runtime: ctx.panel shows one component right of the screen's body (about 40%,
hidden below 100 columns, dock stays full width). F2 moves the focus into it
(viewport and select keys scroll it, Escape returns), /close-panel closes it.
Closes on conversation switch and facet reload, like overlays.
- Artifacts: Enter opens the artifact in the panel when the terminal fits it,
else inline as before; the panel follows the artifact's newest version.
- Fix: /artifacts rows are keyed by name and version, so every version of an
artifact can be selected.
Pages and TUIs loaded before a deploy hold the contracts they were built
with; without accepts they wait unbound (contract_mismatch) until they
reload. The histories are now written with evolve from the released
versions, and the providers accept them:
- Sessions 3 (rows without main and branches) and 4 (a null branch label
reads as the session title); v4's treeSelf arguments pass as they are
- Catalog 2 and 3, the same row mappings
- SessionTree 1: the tree without labels (no branches, no node
conversation); rename is new and needs nothing
test/p2/contract-versions.test.ts binds the released declarations
against the current server.
Archiving from the session sheet navigates away first, which unmounted SessionOptions and released its Sessions
reference; Chord cancels a released reference's calls, so the archive call rejected with "p2.sessions: released",
shown as the composer toast over the owner-notice lane. The archive now holds a reference of its own until it is done,
and the server runs its steps in a root of its own.
The archive stopped the worker with Hosts.stop, which restarts it for its holders: the page's lingering
conversation started a worker the archived session refused (resolve_failed, "Worker: Session is archived" notice).
SessionWorkerControl.suspend stops it and leaves its holders suspended until a restore (retry) or their release;
archive, a failed fork's archived copy and an archived session's index rebuild use it.
The setting was saved but never read: thinking always started hidden.
It now sets the view whenever its effective value changes (unset = hidden);
the toggle overrides it until then and survives reloads.
A manager on the p2 server owns every MCP connection and process. Session
workers hold one view each and lease the locations their conversations
work at; the manager runs the owner's mcp.json servers there (stdio per
session and location, or shared; HTTP shared by default) and offers their
tools as deferred Durable tools found through tool_search.
Stdio servers on an environment start through its pi-env daemon, one
connection per environment shared by its instances, through cmd.exe on
Windows. A project's .pi/mcp.json, read by the worker at each location,
may add stdio servers that run there and switch the owner's servers on or
off; the manager refuses everything else. A lease gets servers only on
environments its session works on.
p2 (Pi Mobile on Chord v2) moves from pi-private into pi as one commit. Root scripts that loop all workspaces
use --if-present; npm test, ./test.sh and the release script cover packages/* only (p2: cd apps/p2 && npm test);
sync-versions also updates apps' workspace pins.