pi-parley

Conversations between Pi coding-agent sessions: named peers, asks and replies, honest delivery receipts, and sessions on your other computers over SSH.

Packages

Package details

extensionskill

Install pi-parley from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-parley
Package
pi-parley
Version
1.8.0
Published
Oct 29, 2026
Downloads
715/mo · 608/wk
Author
scottmeyer
License
MIT
Types
extension, skill
Size
1.2 MB
Dependencies
2 dependencies · 3 peers
Pi manifest JSON
{
  "skills": [
    "./skills"
  ],
  "extensions": [
    "./extension.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

Pi Parley

Parley is a durable conversation layer for Pi sessions: targeted messaging, explicit asks and replies, honest delivery receipts, and opt-in broker federation for remote peers — whether you're driving the conversation or letting agents coordinate. Local sessions connect automatically.

/parley opens the session picker and message composer. Alt+M does the same where the terminal sends Alt; on macOS that usually needs "Option as Meta", otherwise Option+M types µ. Agents communicate through the parley tool.

Why

Sometimes you're running multiple pi sessions — one researching, one executing, one reviewing. Pi Parley lets you:

  • User-driven orchestration — Send context or findings from your research session to your execution session
  • Agent collaboration — An agent can reach out to another session when it needs help or wants to share results
  • Session awareness — See what other pi sessions are running, their concise current focus, and live status

Unlike pi-messenger (a shared chat room for multi-agent swarms), parley is optimized for chosen recipients. Group sends have independent outcomes; host-local broadcast reaches every live peer visible to the caller.

Parley also integrates with pi-subagents, providing scoped child visibility and a fallback supervisor channel when no native channel is available.

In One Minute

Each pi session that has parley loaded and enabled connects to a tiny local broker over a local IPC transport. The broker keeps track of connected sessions and routes an independent direct message to each session you target by name or session ID. The extension gives you both a tool (parley) and a small overlay UI (/parley or Alt+M). Incoming messages are rendered inline inside the recipient session, can trigger a turn immediately by default, and are also stored in Pi session history as extension entries. If you want a stricter local trust posture, inboundTrigger can reduce or disable auto-triggering.

Install

Install the package:

pi install npm:pi-parley

Restart Pi. Enabled sessions connect automatically, and the package supplies the pi-parley skill with conversation and delivery context.

For local development, register the checkout path instead of an npm copy:

pi install /absolute/path/to/pi-parley

This loads the working tree. Reload a session after editing extension code; broker changes also need a broker restart.

Pi compatibility

This package supports both Pi distributions:

  • @mariozechner/pi-coding-agent 0.73.1
  • @earendil-works/pi-coding-agent 0.80.3 or newer with a compatible sibling-package set; 0.80.3 is tested with pi-agent-core, pi-ai, and pi-tui pinned to 0.80.3, while 0.85.1 and the current 1.0.0 release are tested with their default resolution

Parley also loads in Oh My Pi (OMP, checked with 18.12.6), which maps the same imports to its own host modules. scripts/omp-compat/run.sh starts two OMP sessions against a scripted model and checks that one can ask the other and get the reply; scripts/omp-compat/link.sh checks that an OMP session can link another computer over SSH and see its sessions. Under OMP's Bun runtime, clients skip the native startup lock, which crashes Bun; the broker itself always runs under Node and keeps the real ownership lock.

Installing pi-parley does not install or replace either coding-agent distribution or duplicate its host libraries. Pi, TUI, and TypeBox are optional peers supplied by the host; Runtime dependencies are tsx, used by the standalone broker, and fs-native-extensions, which supplies OS-managed file locks. The native addon ships prebuilt binaries for macOS, Linux and Windows on x64/arm64; Linux requires kernel 3.15 or newer. The fork loader maps the upstream-compatible extension imports to its own host modules. Fork hosts publish name changes to extensions immediately; upstream 0.73.1 exposes the same core event only to RPC/TUI consumers, so pi-parley uses a one-second compatibility fallback there. npm run test:host-compat packs the extension and boots it under upstream 0.73.1, a coherent fork 0.80.3 dependency set, fork 0.85.1, and fork 1.0.0 without allowing one coding-agent distribution to pull in the other.

The child-session integration is optional at runtime. An ordinary Pi session without pi-subagents bridge metadata gets normal parley behavior. Child-only visibility and the fallback contact_supervisor tool activate only when pi-subagents provides the corresponding environment metadata; if its native supervisor channel is available, pi-parley leaves that tool to the native channel.

A session becomes parley-connected when all of these are true:

  • the pi-parley extension is installed and loaded in that session
  • enabled is not set to false in the parley config file, which defaults to ~/.pi/agent/parley/config.json
  • the session has started or reloaded after the extension was installed
  • the local broker is running or can be auto-started

The session list shows connected sessions visible through the caller's routing scope and subagent permissions, not every open Pi process. Remote rows identify their origin and negotiated support: text conversations, text sends only, or discovery only. Remote attachments, mailboxes, and broadcast are not supported.

If a session is unnamed, pi-parley exposes a collision-resistant runtime-only fallback alias like session-1a2b3c4d-5e6f-7a8b so other connected sessions can target it. That alias is not persisted as the Pi session title or treated as a reconnect identity, so pi --resume can keep showing the transcript snippet without allowing a different unnamed process to inherit queued mail.

Identity and focus

/alias edits the current session's persisted Pi name; with no argument it opens an input in interactive mode. The rename tool action accepts name for the same canonical identity change. Neither renames another session. Send/ask call displays and delivery results include the sender identity used for that contact.

Any parley action accepts an optional profile with name and/or description. Descriptions are 5–9 word focus labels; a new one replaces the old. Empty or null fields are ignored, because some models fill every optional field with a placeholder, so a profile placeholder never clears anything. A profile name can fill an unnamed/generated identity or revise a profile-managed name, but cannot replace an explicit user or host name. Results distinguish the canonical Pi name, broker-confirmed parley identity, focus, and publication status. Focus is display metadata, not a routing or permission boundary.

Conversations

send shares information; ask requests an answer; reply communicates back to the colleague. Questions can wait for an answer in the tool call or receive it later while work continues. A notification stays a notification even when another question is pending, and colleagues can consult each other without closing the original question.

Messages carry the sender, the text, and related conversation context. Receipts include the message reference and observed delivery state. Endpoint acceptance, a colleague's answer, and completed work are different events. Unknown delivery may mean the message arrived but its acknowledgement did not.

A message the person typed in the /parley composer is labelled as written by the person at the sending session, not its agent. The label is the sending session's own claim. It reaches local readers that understand it. Older local readers get the plain message, and remote peers currently receive text only.

References

Models, like people, work with names. Everything a model reads or types names a session by its name and a message by a local number (#12). A session on this computer is named as-is (pi-parley:Ellis). A session on another computer carries that computer's label first (build-vm:pi-parley:Ellis), so the same name on two machines never collides. Canonical session and message IDs, including origin-qualified federation handles, stay on the wire and in result details, and parley translates in both directions.

A reference stays attached to what it first named. It is never recycled, whether through a rename, a reconnect, a reload, or a different session later taking the same name. That newcomer gets name~2 instead. Unnamed sessions appear as unnamed, unnamed~2, and hosts without a label as remote-1, never as ID fragments.

Parley routes a reference only to the identity it names, never back through a name or ID prefix that could match someone else. A live target is addressed by its exact endpoint. An absent local target uses the broker's exact-identity mode (exact-identity-send-v1), which looks up only that identity's disconnected entry. A broker that predates the mode refuses before anything is sent (E_EXACT_IDENTITY_UNSUPPORTED) rather than falling back to a looser lookup. Federated targets are already addressed exactly. Delivery to an absent session still follows the broker's offline-mail rule: queued mail may be handed to the one live session using the same name in the same directory, as it always has. When that happens the result says so. When a session you address is unreachable and another session uses its name, the result names that session as a choice and, if your message was queued, tells you how to withdraw it. An unknown outcome is reported as unknown. A project launch that chooses a new session says it was a project launch.

Message numbers are local to a session, so your #12 is not your colleague's #12. Threading still crosses the wire exactly, through replyTo. A message can also carry local labels: label names an existing message (sent or received), and label on send or ask names the new one. Labels appear next to the number (#12 · review) and work wherever a message reference does.

An earlier request whose reply window has elapsed, or whose sender is currently unreachable, is mentioned once in automatic conversation context and then only counted. pending and status always list everything. Waiting is not settlement: a request absent from a flapping roster is unreachable, not ended.

Incoming content can wake an idle session or join a busy session's next model turn, including headless sessions. Attachment names label inline text snapshots, not files created in the receiving workspace.

Retained context

Action results include a small view of pending conversations. pending, status, and read expose unanswered requests, outgoing questions, and retained message text. Session history supports reload/resume recovery, including interrupted delivery; it is not a promise of exactly-once processing.

The local wait normally ends after ten minutes (PI_PARLEY_ASK_TIMEOUT_MS), but that does not withdraw the question. Cancellation removes undelivered mail or communicates withdrawal; work already performed remains unchanged.

Local history and broker routing have different lifetimes. Thread relationships survive endpoint reconnects but remain bounded broker memory (up to one hour and 4,096 recent relationships). Offline mail lasts up to 24 hours while the broker remains running. Neither survives a broker restart. A locally retained message may therefore remain readable after its thread is no longer authorized.

Tool Reference

parley

Parameter Type Description
action string "list", "list-cwd", "send", "broadcast", "ask", "reply", "pending", "read", "status", "cancel", or "rename"
to string One session reference, as shown by list, receipts, or messages. Without cwd, send/ask resolve it within the visible roster. With cwd, send/ask require the target to be in that directory. Also narrows reply.
targets string[] For send, 1–32 session references. Each recipient gets an independent message and delivery outcome. Cannot be combined with to, cwd targeting, or conversation-specific reply/retry/supersede fields.
message string Message text (for send/broadcast/ask/reply)
attachments array Inline text snapshots: { type: "file" | "snippet" | "context", name, content, language? }. No files are created at the destination.
replyTo string Message reference (like #12) for conversation threading. reply explicitly answers; a threaded send remains a notification.
messageId string Message reference for cancel, retained-message read, or label
label string A one-word local name for a message. With label, names messageId; with send/ask, names the new message.
supersedes string Optional reference of an earlier send/ask that this one explicitly replaces
retryOf string Optional reference of an earlier send/ask that this one explicitly retries
cwd string Working directory filter for list-cwd. For send/ask, scopes target lookup to that directory; without to, selects the sole live peer there.
blocking boolean For ask, true waits for the answer (default); false returns the initial delivery outcome and receives the answer later in the conversation
openProjectPaneIfMissing boolean For send/ask with cwd, launch Pi in that project through a registered generic project launcher when no matching live session exists
focus boolean For openProjectPaneIfMissing, focus the new terminal when the launcher supports it. Defaults to true
name string Canonical self-name for rename
profile object Optional self-profile update: { name?, description? }; empty or null fields are ignored

contact_supervisor

Registered only with the required pi-subagents child bridge metadata and no native supervisor channel. Contacts the supervisor session that delegated the current task.

Parameter Type Description
reason string "need_decision" (blocking), "interview_request" (blocking structured questions), or "progress_update" (fire-and-forget)
message string The decision request, optional interview note, or progress update
interview object Required for interview_request: { title?, description?, questions: [...] }

need_decision — Sends a formatted ask to the supervisor and blocks until it replies (10-minute timeout by default; configurable with PI_PARLEY_ASK_TIMEOUT_MS). The reply comes back as the tool result. Includes run metadata in the message so the supervisor knows which subagent is asking.

interview_request — Sends a formatted, agent-readable interview to the supervisor and blocks until it replies. Questions use a local pi-interview-like shape: { id, type, question, options?, context? } where type is single, multi, text, image, or info. info questions are context-only and do not need responses. The supervisor reply should be JSON with { "responses": [{ "id": "...", "value": ... }] }. Validated responses are returned in details.structuredReply; parse or validation failures are also reported in the visible result text.

progress_update — Sends a non-blocking update to the supervisor and returns the delivery outcome, not a supervisor answer. Intended for meaningful discoveries that change the plan.

parley actions

list / list-cwd — Returns the current session and visible connected peers by reference, with directory, focus, model, and activity. list-cwd filters by directory. Activity follows Pi lifecycle events: idle, thinking, or tool:<name>. Presence changes do not wake peers. Federation rows indicate their remote capabilities.

send — Sends to one to or independently to 1–32 explicit targets. Duplicate aliases for one live endpoint deliver once; partial failures do not roll back successes. Group sends reject replyTo, supersedes, and retryOf. A singular send can carry thread context, but never infers or completes an ask's answer. confirmSend can require one UI approval before ordinary delivery; group approval pins the resolved endpoints so an alias cannot silently rebind afterward.

broadcast — Sends independent messages to the current snapshot of visible live host-local peers, excluding the sender. Scope and subagent permissions still apply. It neither includes remote rows nor queues disconnected sessions, and rejects targeting/thread fields.

ask — Sends a question to a connected local recipient or a negotiated remote conversation endpoint. It waits for the explicit answer by default, or returns the initial delivery outcome with blocking: false. Asynchronous answers enter the conversation later, including while a headless caller works. Offline asks fail rather than entering a mailbox. Timeout and cancellation are separate outcomes.

reply — Explicitly answers a pending ask or threads a response to an ordinary message. It uses the active parley context, otherwise the sole pending ask; to can select a sender and exact replyTo can select a retained message. Ambiguity returns candidate context rather than guessing. A clarification ask does not complete the original question.

pending / read — pending shows every unanswered request, with message references and reply-window state. read returns one retained incoming message's full text and attachment snapshots by messageId; it is not an archive search or remote-file read.

label — Gives a message a session-local name, like review, usable anywhere a message reference is. Labels persist with the session and never replace the message's number.

cancel — Operates on a message this session previously sent. It distinguishes offline removal, known nondelivery, and withdrawal requested from a live recipient. Unknown outcomes remain unknown. A visible withdrawal or supersession notice does not erase earlier messages or undo work.

status — Reports connectivity, the current session's reference, visible connected-session count, and locally tracked outstanding questions, including requests whose local wait has ended. Local age is not an authoritative broker completion signal.

rename — rename sets this session's canonical Pi name through name. Subagents are scoped to their supervisor tree.

link / unlink — Links this computer with others over SSH (see Other computers). host adds one and answers once it's linked or says why not; recheck checks one or all again; discover turns automatic linking on or off; with none of these it shows the links. unlink stops linking one computer and forgets it.

Directory targeting and project launch

For send/ask, to alone resolves within the visible roster. cwd addresses a local directory: alone it selects the sole local peer there; with to it constrains lookup to that directory. openProjectPaneIfMissing: true enables a launch attempt when no matching peer exists.

Parley uses a generic launcher, not a built-in terminal manager. It selects a visible local session advertising pi-parley/project-launch-v1; otherwise it uses the configured PI_PARLEY_PROJECT_LAUNCHER (or config projectLauncher). With neither, no launch is attempted.

The provider API receives an ordinary parley message containing { type: "pi-parley/project-launch-request", root, command, focus }. A configured command receives the raw root in PI_PARLEY_PROJECT_ROOT; a bare {root} placeholder is replaced with a shell-quoted path. There is no default command.

Results preserve separate observations: launch request accepted or command started, new peer registration observed, and message accepted. A later failure does not erase earlier stages. The v1 request carries no requested name or registration correlation: it observes a sole new local peer in the project, cannot prove that a particular launch created it, and reports ambiguity when several appear. Ending the registration wait does not cancel startup; repeating a launch can create another visible surface.

Other computers

Parley links this computer with the others you work on over SSH, so their sessions show up here and yours show up there. It uses your own ssh: nothing is installed or opened on the network, and a computer needs only Pi with Parley. It's on by default for interactive sessions, except in FlightDeck tabs (FlightDeck links those itself) and subagents; headless runs, scripts and test runners don't go looking unless asked. /parley link off turns it off for this computer, and "linkByDefault": false in config.json turns the default off.

/parley link on                 # link computers this one already has ssh connections to (the default)
/parley link add build-vm       # or name one, as you'd type it after ssh
/parley link                    # what's linked, and why anything isn't
/parley link recheck [host]     # check again now, e.g. after starting Pi there
/parley link remove build-vm    # stop linking it, and don't find it again

/parley completes as you type, through the verbs and then the hosts in your ssh config. /intercom is the same command, for people coming from pi-intercom.

With discovery on, Parley notices the computers you're already connected to (a terminal running ssh build-vm, an editor's remote mode, the machines saved in herdr) and links the ones that have Parley. You get a notification, "Parley linked build-vm"; the model is told nothing. Linked computers are remembered and kept linked, retrying when they're asleep or unreachable. Tools that use ssh as a pipe, such as git, rsync and scp, are left alone, as are port forwards and other one-off options; only the options that say where and as whom are repeated.

Sessions on this computer keep their own names. Sessions elsewhere carry their computer's label: build-vm:pi-parley:Ellis. Every computer is linked with every other one, through this one where needed, and every computer is "this computer" to itself; none is the main one.

A computer has to be reachable without a prompt: if ssh build-vm asks for a password or a host-key decision, Parley says so instead of asking. It links the sessions FlightDeck and other applications haven't put in a scope of their own (PI_PARLEY_SCOPE_ID); those applications link their scopes themselves, alongside.

Each computer runs one small link keeper while Pi sessions are open there. It holds the links, shares one SSH connection per computer, and stops shortly after the last session ends. Its state lives in parley/links.json, its log in parley/link.log.

Claude Code and Codex

Claude Code and Codex sessions can join Parley too, in their terminals and in their desktop apps (Claude Desktop's Code tab, Codex in the ChatGPT app), which share the terminals' configuration. The simplest way is the plugin, installed from this repository:

# Claude Code (terminal or Claude Desktop's Code tab)
claude plugin marketplace add Scott-Meyer/pi-parley
claude plugin install parley@pi-parley

# Codex (terminal, ChatGPT app)
codex plugin marketplace add Scott-Meyer/pi-parley
codex plugin add parley@pi-parley

Then start a new session. Codex asks you to trust the plugin's hooks once (it asks again after an update); trust the four that run parley.mjs … hook. A Codex plugin installed while Codex's background app server is running takes effect when that server restarts (codex app-server daemon restart, or a reboot). The first time Codex uses the tool it asks for approval; "Always allow" keeps it from asking again. Only Node.js needs to be installed.

Alternatively, with Parley installed from npm, connect sets it up through the hosts' own configuration instead of a plugin. In Codex that also passes the session's FlightDeck and herdr names through and skips the approval prompt:

npm install -g pi-parley
parley connect claude      # or: parley connect codex

connect registers Parley's MCP server with the host and adds four hooks marked _parley, next to any you (or FlightDeck) already have; the files are backed up to *.before-parley, and parley disconnect claude|codex removes exactly Parley's parts. Codex asks you to trust new hooks once: run /hooks in Codex and trust the four that run parley.mjs hook. New sessions then appear in Parley like any Pi session (in a FlightDeck tab, as workspace:tab; elsewhere folder:claude or folder:codex), with the same parley tool, references, receipts and links.

Inside, it's the same Parley extension a Pi session runs, on a small stand-in for Pi's extension API. What differs per host is how a message reaches an agent that isn't calling the tool:

Host An idle session A busy one
Claude Code woken through Claude's own cross-session inbox (the one its SendMessage uses) the same; hooks hand over anything the inbox didn't take
Codex woken with codex queue; a brand-new session can't be woken until its first prompt, so a message waits for it handed over when the turn ends (Stop hook) or with the next prompt

Claude's inbox is internal to Claude Code and may change between releases; Claude Code's official equivalent, channels, is in research preview and limited to allowlisted plugins. Claude Desktop's chat and Cowork modes don't read Claude Code's configuration and don't get Parley.

herdr

In a herdr pane, a session is named after where it lives, <workspace>:<tab> (or the pane's own label when it has one), the way FlightDeck names its tabs' sessions. Renaming the workspace, tab or pane in herdr renames the session, and renaming the session (/name, /alias, the parley tool's rename) renames herdr's: the tab when the session's pane is the tab's only one, otherwise just that pane's label, so another pane's tab never changes under it. A resumed session keeps its name and herdr follows. Subagents in the pane get <workspace>:<tab>:<name> and rename nothing. "herdrNames": false in config.json turns this off.

Nobody's context is shared

Parley never tells a session anything about another session's context: no usage numbers and no compacting status. The one exception is a small note on your own receipt: when you send to a colleague you've talked to before and they've compacted since, the receipt says so once ("Note: Hazel has compacted since you last talked. It probably still knows what's going on, but if this depends on something specific from before, its memory of that may be vaguer."). The receiving side is never told anything about the sender.

Keyboard Shortcuts

Key Action
/parley (or Alt+M) Open session list overlay. On macOS, Alt+M needs the terminal's "Option as Meta" setting
↑/↓ Navigate session list
Enter Select session / Send message
Escape Cancel / Close overlay

Config

Create ~/.pi/agent/parley/config.json:

{
  "brokerCommand": "npx",
  "brokerArgs": ["--no-install", "tsx"],
  "confirmSend": false,
  "inboundTrigger": "always",
  "enabled": true,
  "replyHint": true,
  "status": "researching"
}
Setting Default Description
brokerCommand "npx" Advanced trusted override for the broker executable. The default value is hardened internally to launch the resolved bundled tsx CLI through the current Node executable instead of resolving npx through PATH.
brokerArgs ["--no-install", "tsx"] Advanced trusted arguments passed to custom brokerCommand before the broker script path
confirmSend false Show a confirmation dialog before ordinary sends from an interactive session with UI; caller-supplied replyTo skips it
inboundTrigger "always" Auto-trigger policy for unsolicited broker messages: "always", "replies", or "never". Requested asynchronous answers, withdrawal notices, and local in-process subagent relay events have explicit delivery paths.
enabled true Enable/disable parley entirely
linkByDefault true Interactive sessions link computers this one has ssh connections to, unless /parley link off was used or the session is in a FlightDeck tab or a subagent. PI_PARLEY_LINK_BY_DEFAULT=0 overrides it for one process tree
herdrNames true In a herdr pane, keep the session's name and herdr's names in step
replyHint true Include a reply affordance for incoming asks in the rendered message
status — Optional custom status suffix shown after the automatic lifecycle status, for example thinking · researching

If config.json cannot be parsed or contains an invalid value, pi-parley logs the error and uses safe defaults: inboundTrigger: "never" and confirmSend: true until the config is fixed. Explicit requested-answer and control-notice delivery still applies. A valid explicit enabled: false remains respected even if another setting is invalid; outbox sends without a confirmation UI stay blocked. Obsolete toolVisibility values are ignored; the generic parley tool remains stable in the active tool set for prompt-cache friendliness.

Custom broker commands are trusted local configuration: anyone who can edit this config can choose the executable used for future broker auto-spawns. For example, if you have Bun installed and want it to start the broker directly, use:

{
  "brokerCommand": "bun",
  "brokerArgs": []
}

Pi Parley publishes live session status automatically. Sessions register as idle, switch to thinking while the agent is running, and show tool:<name> during tool execution. This is passive roster presence and never wakes peer agents. If status is set in config, it is appended as context instead of replacing the lifecycle status.

Set PI_PARLEY_SCOPE_ID before starting Pi to opt a session into an opaque broker routing scope. The value is trimmed. Empty values are treated as unscoped. A scoped session can list, address by full ID, name, ID prefix, or cwd, receive presence and session lifecycle events, recover queued mailbox messages, and use extension-channel owner, publish, and state traffic only with sessions that registered the exact same scope. Scoped sessions and unscoped sessions do not cross this boundary. Existing unscoped behavior is unchanged when the variable is not set.

By default, runtime state and config live under ~/.pi/agent/parley. If Pi is launched with PI_CODING_AGENT_DIR, pi-parley uses $PI_CODING_AGENT_DIR/parley instead, including config.json, broker PID/lock files, sockets, and launcher state.

Extension channels

Other Pi extensions can use parley's broker for bounded, non-conversational coordination. Extension-channel traffic never calls pi.sendMessage(), never enters a session transcript, and never starts an agent turn.

Register during session_start so parley includes the capability in its deferred broker registration:

// Use @earendil-works/pi-coding-agent here when targeting that distribution.
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import {
  PARLEY_EXTENSION_REGISTER_EVENT,
  type ParleyExtensionChannel,
} from "pi-parley/extension-api.ts";

export default function (pi: ExtensionAPI) {
  let channel: ParleyExtensionChannel | undefined;

  pi.on("session_start", () => {
    pi.events.emit(PARLEY_EXTENSION_REGISTER_EVENT, {
      namespace: "example/v1",
      ownerEligible: true,
      onReady: (value: ParleyExtensionChannel) => { channel = value; },
      onEvent: (event: unknown) => { /* owner, state, peer, or payload event */ },
    });
  });
}

The broker:

  • advertises extension-bus-v1 through feature negotiation
  • routes payloads only to sessions advertising the same namespace
  • elects one owner per namespace and changes its epoch after socket replacement
  • rejects stale owner-only writes
  • stores at most 64 KiB of opaque, revisioned state per namespace

channel.publish() accepts payloads up to 16 KiB. A capable broadcast includes the sender, so consumers must not blindly republish messages they receive. channel.commitState() uses compare-and-swap against the last observed revision. Capabilities registered after the broker connection is established are synchronized without reconnecting. If the broker does not advertise extension-channel support, clients do not send extension operations.

Extension outbox

Same-process extensions can request a user-visible parley send through the consent-aware outbox. Emit parley:outbox-request with a unique requestId; listen for parley:outbox-result and treat sent, rejected, blocked, and failed as terminal states. There is no fire-and-forget mode.

import {
  PARLEY_OUTBOX_REQUEST_EVENT,
  PARLEY_OUTBOX_RESULT_EVENT,
  type ParleyOutboxResult,
} from "pi-parley/extension-api.ts";

pi.events.on(PARLEY_OUTBOX_RESULT_EVENT, (result: ParleyOutboxResult) => {
  if (result.requestId === "example-request-1") {
    // Handle the terminal result.
  }
});

pi.events.emit(PARLEY_OUTBOX_REQUEST_EVENT, {
  version: 1,
  requestId: "example-request-1",
  extensionId: "example-extension",
  extensionName: "Example Extension",
  to: "planner",
  message: "Build finished.",
});

confirmSend applies to outbox requests. If confirmation is required and no UI is available, the request fails closed with confirmation_unavailable. The outbox resolves the target through the current session's scoped parley client, so extensions cannot choose the sender, scope, or resolved target ID. Duplicate requestId values are rejected and do not deliver again. Receiver messages include structured extension_outbox provenance in message details and model-visible sender context.

Embedded actor extension

Applications that must enroll every Pi actor can bundle the complete package and call the explicit Pi-only TypeScript entrypoint from their required extension wrapper:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { registerParleyExtension } from "pi-parley/extension";

export default function applicationExtension(pi: ExtensionAPI) {
  registerApplicationStatus(pi);
  registerParleyExtension(pi, {
    resolvePresenceName(candidate) {
      return `application:${candidate ?? "session"}`;
    },
  });
}

The optional synchronous resolvePresenceName(candidate, context) policy controls every broker-visible name while leaving Parley unaware of the application's naming scheme. candidate is the canonical Pi session name or undefined; context.kind is "session" for initial connection, reconnect, compatibility-polled host renames, /alias, self rename, and ordinary presence updates. The trimmed nonempty result becomes the broker/client name. Keep the resolver deterministic and side-effect-free.

Call registerParleyExtension() once as the wrapper factory's final potentially throwing operation, not from session_start, and load the required wrapper before optional user packages. Register the application's own resources first and do not throw after Parley returns: older supported Pi hosts do not retract event subscriptions when an outer extension factory later fails. Registration is idempotent across physical copies that implement this v1 actor entrypoint in one Pi runtime; an ambient-first v1 owner accepts a later wrapper's resolver before its first identity publication, while conflicting or late configuration fails closed. Shutdown releases that runtime claim so reload and session replacement bind fresh handlers. An older ambient pi-parley release cannot participate in the claim protocol and must be updated or excluded before an application forces its bundled actor. The factory starts no process, socket, watcher, or timer. Session-scoped work starts from lifecycle events or the first operation and is joined by Parley's session_shutdown handler; the wrapper owns no Parley teardown.

Install the bundled tarball as the wrapper's private ordinary dependency rather than as another Pi package. The complete tarball and its production dependencies are required because the actor uses Parley's TypeScript extension, UI, client, broker, and spawn modules. Pi supplies the peer extension-runtime, TUI, and TypeBox modules. The application must set generic routing such as PI_PARLEY_SCOPE_ID before Pi loads extensions.

Embedding Parley in another application

pi-parley/host runs Parley sessions inside an application that isn't Pi. It's new, and its shape may still change in a minor release while its first embedders settle it. It's how the Claude Code and Codex support works, and an application can use it directly. Each of the application's agent sessions gets a hosted session: the same Parley a Pi session has (the parley tool, references, receipts, history, names, links), joined under the id the application gives it. Many can share one process.

import { configureParleyHost, createHostedSession, runBroker } from "pi-parley/host";

const session = await createHostedSession({
  cwd, defaultName: () => chat.title, app: () => "My App",
  stateFile: (id) => join(dataDir, `${id}.jsonl`),
  env: {}, publishSessionEnv: false,          // one of many sessions in this process
  deliver: (delivery) => chat.post(delivery), // returns { accepted: true } or { accepted: false, reason }
  nameChanged: (name) => chat.rename(name),
});
await session.begin(chat.id);
  • Tool: session.tool is the parley tool's name, description and JSON Schema, and session.call(args, signal) runs it, returning { text, isError, details }.
  • Deliveries: deliver gets each thing Parley would put in front of the model: { text, wake, during?, customType, details, display? }. text is for the model; display is for a person, with the colleague's own words and a preview of the message they answer ({ text, inReplyTo? }). wake asks for a turn to start; during says how to place it in a running turn. It counts as shown only when the application accepts it, synchronously or through a promise. Until then the sender hears it's queued, and Parley offers it again with backoff and adds it to the next model call. Other notices (compaction awareness, delivery expiry) are best effort: a refused one is dropped. Once the application has taken a delivery, a bare reply in that run answers it; taken while another is already being answered, it's one more candidate, so a bare reply asks which.
  • The model boundary: optionally, session.context(messages) takes the messages about to go to the model, in Pi's shape, and returns the ones to send. It makes references readable, drops duplicate copies of a message, adds anything not yet accepted, and marks which messages are being answered. Keep accepted deliveries as custom messages (customType, details) to get all of it.
  • Lifecycle: setBusy(busy) at turn start and end, setName when the person renames the session, setModel, compaction("start" | "end" | "failed"), and stop(). One live hosted session per id: stop the old one before a replacement begins with the same id. The replacement keeps the session's identity, names and history from stateFile.
  • Isolation: a hosted session takes its identity only from begin(id), never from PI_PARLEY_STABLE_ID or a configured stableId. With env, it reads its FlightDeck, herdr, scope and linking settings from that object instead of the process's. With publishSessionEnv: false, it never writes the process environment.
  • The broker: sessions use the computer's broker, the same one Pi, Claude Code and Codex sessions use, and start it if none is running. An application that can't run Parley's broker script tells the process how to start one, with configureParleyHost({ brokerLauncher: { command, args, env? }, startupLock?: false, lockAddon? }), and runs it from its own entry with runBroker(). lockAddon is the path to fs-native-extensions' native .node binary, for a bundle that ships it alone.
  • Bundling: a bundler can take pi-parley/host whole. pi-parley/host/bundle-define gives esbuild the define that stamps in the identity of the Parley sources being bundled: define: parleyBundleDefine(). Some bundled dependencies call require(), so an ESM bundle needs a createRequire banner.

Broker federation facade

Cross-computer transport is caller-loaded and opt-in. Parley does not discover hosts, approve topology, import a transport provider, or reconnect links. The compiled pi-parley/federation entrypoint works in plain Node ESM without a TypeScript loader and keeps broker paths, endpoint credentials, control frames, origins, and handshake machinery behind one public boundary.

import {
  inspectBroker,
  attachBrokers,
  type BrokerHostAccess,
  type ScopeBinding,
} from "pi-parley/federation";

const laptop = await inspectBroker(laptopAccess, { signal });
const server = await inspectBroker(serverAccess, { signal });

const laptopScopes: ScopeBinding[] = [{
  localScopeId: "team",
  localScopeAlias: "laptop",
  remoteScopeAlias: "server",
}];
const serverScopes: ScopeBinding[] = [{
  localScopeId: "team",
  localScopeAlias: "server",
  remoteScopeAlias: "laptop",
}];

const link = await attachBrokers({
  initiator: { broker: laptop, originLabel: "Laptop", scopeBindings: laptopScopes },
  acceptor: { broker: server, originLabel: "Server", scopeBindings: serverScopes },
  signal,
  timeoutMs: 10_000,
});

await link.close();

A BrokerHostAccess is rooted at one Pi agent directory. It offers bounded reads relative to that root and opens only exact host-local Unix sockets, named pipes, or loopback TCP endpoints requested by Parley. createLocalBrokerAccess() provides this capability for the current machine; remote-management applications can adapt their existing authenticated host streams. TCP state credentials never leave the facade in endpoints, handles, JSON, or errors.

Authority is reachability of a broker's local endpoint. Opt-in TCP brokers require their state credential, but Unix sockets and named pipes rely only on OS permissions. Whatever can open one can register sessions and prepare peer links. A carrier should give the controller a path to the remote broker. It should not publish a broker's socket as a port other processes can reach.

inspectBroker() returns a frozen, runtime-opaque handle containing the canonical Parley installation origin and an immutable live-scope snapshot. It owns no persistent stream. Inspection may mint the installation's first federation origin, so caller consent belongs before inspection. A TCP handle becomes stale after broker restart.

attachBrokers() validates both handles and independently supplied scope mappings before opening either endpoint. It opens both streams concurrently under one deadline, aborts a sibling acquisition after failure, joins late streams, and transfers ownership to the brokers only after both opens succeed. It never rediscovers, retries, reconnects, or replays. Readiness means both brokers accepted the handshake. The returned attachment has a non-rejecting completion and idempotent close(); both join admitted writes and owned stream cleanup. Sanitized BrokerInspectionError and BrokerAttachmentError codes contain no provider diagnostics, paths, credentials, or raw frames.

How It Works

graph TB
    subgraph A["Pi Session A"]
        A1[Parley Client]
        A2[parley tool]
        A3[UI overlays]
    end

    subgraph Broker["Parley Broker"]
        B1[Session Registry]
        B2[Message Router]
    end

    subgraph B["Pi Session B"]
        B3[Parley Client]
        B4[parley tool]
        B5[UI overlays]
    end

    A1 <-->|Local Socket/Pipe| B1
    B1 --- B2
    B2 <-->|Local Socket/Pipe| B3

The broker is a standalone TypeScript process that manages session registration and message routing. It auto-spawns when the first parley-enabled session needs it and exits after 5 seconds when the last connected session and peer link disconnect. Clients now reconnect automatically if the broker disappears and later comes back.

Liveness heartbeat. Transport loss can leave a connection apparently open without a responsive broker. Each registered client round-trips a lightweight list request and tears down the socket if the broker does not respond within the timeout, allowing reconnection. The interval defaults to 30s and the probe timeout to 5s; override them with PI_PARLEY_LIVENESS_INTERVAL_MS and PI_PARLEY_LIVENESS_TIMEOUT_MS (the timeout is clamped to the interval).

Messages use length-prefixed JSON over a local socket/pipe transport (4-byte length + JSON payload) to handle fragmentation properly. The protocol includes request correlation for session listing, explicit delivery failures, validation for malformed or out-of-order messages, a frame-size cap, per-connection local rate limiting, and no-op presence coalescing.

Experimental broker federation. A caller-owned authenticated stream attaches two brokers without making either broker network-addressable. The neutral attachment controller handles the single-use bridge_attach preface and independently authorized broker_accept_peer preparation; that exact prepared destination connection becomes the opaque pipe. Readiness requires the brokers' strict peer_hello / peer_hello_ack handshake. Public scope aliases never expose private local scope IDs, and peer links never impersonate ordinary local clients.

When both brokers negotiate peer-roster-v1, each link exchanges an authoritative bounded snapshot followed by monotonically sequenced deltas under a broker-lifetime origin epoch. Sequence gaps request a fresh snapshot; stale epochs cannot roll state back; and disconnect atomically prunes only that link's imported sessions. Brokers export locally owned mains, never re-export imports. Raw local scope IDs remain link-local authority while public aliases qualify remote identities. Imported rows are visibly marked remote:…, are never trustedLocal, and respect local scope/subagent visibility.

peer-send-v1 supports direct text sends (≤32 KiB) to imported oqs1.* sessions. The destination authenticates the sender through its imported roster and enforces local target ownership, scope, and visibility. A correlated destination result confirms acceptance; a missing acknowledgement does not prove nondelivery. Legacy links remain send-only or discovery-only.

peer-conversation-text-v1 adds text asks, replies, and threaded progress when both brokers and participating endpoint clients support the conversation contract and exact sends. Before an ask installs its waiter/history, prepareConversation() returns a canonical oqm1.* message ID and pinned author/recipient snapshots. Retained IDs include the original author's origin, scope, broker incarnation, session identity, endpoint incarnation, and nonce. Replies retain the original replyTo; only the recorded counterpart with both endpoint and broker incarnations intact may answer. Progress does not complete an ask, and replacement endpoints are not rebound. Ordinary notifications on capable endpoints also retain replyable conversation identities.

A bounded, flushed dispatch journal records attempts before dispatch. Uncertain outcomes survive broker-process crashes/restarts and reconnects without automatic replay; only a positively correlated preacceptance nondelivery verdict can rearm an attempt. Retained uncertainty still blocks an unchanged legacy retry if its name now resolves locally or the target upgrades to conversations; preflight checks the original identity before conversion. Capability downgrade does not turn a retained receipt into known nondelivery. Caller-supplied scalar identities are durably associated with converted handles before preparation returns; association alone is not dispatch, and both identities share one admission record. Retained identities remain unknown/non-retryable when discovery or preflight cannot recover a verdict, while fresh operations remain known-unsent before dispatch. Passive lookup creates no journal; an incomplete final append preserves the verified prefix and allows unrelated local work while disabling new federated dispatch. Unreadable or corrupt complete history is not treated as absence. Persistence failure or exhausted retention fails closed before a new dispatch. This is a broker-process recovery guarantee, not a promise of recovery after host power loss or filesystem loss. If an answer arrives before a lost ask acknowledgement, the blocking tool returns the answer while keeping transport acceptance explicitly unknown.

A broker holds one link per remote broker per set of local scopes. Links carrying different scopes sit side by side, so independent owners (FlightDeck for its scope, the SSH link keeper for ordinary sessions) link the same pair of computers without knowing about each other; both brokers negotiate this as peer-links-by-scope-v1, and a broker that can't hold two refuses the second. A new link for scopes an existing link already carries is a simultaneous attach for its first 10 seconds, settled by preferred direction. After that, the existing link is defended: the newcomer is refused with E_ALREADY_CONNECTED, and the refusal asks the existing link whether anyone is still behind it (peer-ping-v1, or a roster resynchronization request for older peers). A link that stays silent for 3 seconds is closed, so one left lingering by a restarted peer or controller clears for the retry.

The broker owns a persisted canonical federation origin (adopted from the first controller-supplied id or minted as install:<uuid>), exposed with live scope enumeration through the trusted-local broker_list_scopes control; every dial/accept must present exactly it.

Caller-owned endpoint snapshots additionally require peer-send-exact-v1: replacement endpoints fail with E_TARGET_REBOUND, and legacy links reject pinned sends rather than silently delivering to a replacement. Ordinary re-resolvable sends remain compatible with peer-send-v1. Remote attachments, supersession, queued mailboxes, broadcast, extension channels, compaction awareness, and multi-hop routing are outside this slice. Identity transport, roster replication, direct sends, and conversations negotiate independently; rolling older local clients keep their original metadata schema.

Session IDs are the trusted addressing key within one broker routing scope. Duplicate names remain allowed, but ambiguous names fail rather than selecting a recipient. The stable session ID shown by list/status distinguishes those endpoints. Mail queued for a disconnected session is redelivered to a session that reconnects under the same session ID, or to a session that matches both its explicit name and its directory, so a same-named session in a different project never inherits another project's queued messages. Runtime-only session-... aliases are excluded from name-based mailbox reconnection, and a disconnected mailbox is never remapped to the sender. Set PI_PARLEY_STABLE_ID or stableId in config.json to pin a session's parley ID across full process relaunches; config.json is machine-global, so a fixed stableId there applies to every session on the machine and the newest registration takes over that identity only within the same PI_PARLEY_SCOPE_ID boundary. The broker owns local trust metadata such as trustedLocal; peerUid is reserved for runtimes that can expose real peer credentials and is left unset otherwise. Client-supplied cwd/model/pid/status are display metadata, not authentication.

Async extension work (startup, inbound flushes, reconnects, overlays, and relays) no-ops if the session shuts down or reloads before it settles.

Runtime files live at ~/.pi/agent/parley/ by default, or $PI_CODING_AGENT_DIR/parley/ when PI_CODING_AGENT_DIR is set:

  • broker.sock — Unix domain socket for communication (macOS/Linux only; Windows uses a named pipe instead)
  • broker-launch.vbs — Windows helper script used to launch the broker without a console window
  • broker.pid — Broker process ID
  • broker.startup/.process.lock — Permanent advisory startup lock
  • broker.ownership/.process.lock — Permanent broker lifetime lock
  • broker.port.json — Authenticated dynamic localhost endpoint when TCP transport is explicitly enabled
  • config.json — User configuration

Supported config.json keys include stableId for restart-stable addressing, status for a custom status suffix, inboundTrigger (always, replies, or never), replyHint, confirmSend, and advanced broker launch overrides.

Design Decisions

Local broker IPC instead of a listening network service. pi-parley uses Unix sockets on macOS/Linux and a named pipe on Windows, which keeps local setup simple and avoids exposed broker ports. Cross-machine federation delegates authenticated transport and ephemeral loopback attachment to a host controller rather than making the broker network-addressable. Authenticated loopback TCP is an explicit alternative on any platform with PI_PARLEY_TRANSPORT=tcp (or PI_PARLEY_TCP=1), useful when local IPC is unavailable. In that mode the broker binds a dynamic 127.0.0.1 port, records the endpoint plus a local secret under the parley state dir, and requires that secret before health or registration succeeds. Health replies do not echo the secret, so a random localhost process cannot discover it through the broker protocol.

Runtime ownership. One broker owns each runtime directory until shutdown completes. Ownership is an OS-managed file lock, not a PID or an elapsed-time lease: paused owners remain owners, and process exit releases the lock. A separate short-lived startup lock coordinates clients starting that broker. Lock files stay at stable paths and are never deleted to reclaim ownership. The lock files contain no metadata; separate .process.owner.json diagnostics may be stale or incomplete and never establish ownership. Runtime directories must remain on a local filesystem and must not be moved or replaced while in use.

Conversation intent is explicit. The broker correlates ask/reply edges while the client owns blocking waits and asynchronous conversation delivery. Threading alone is not an answer: notifications preserve pending questions, and explicit replies settle them. Request IDs also correlate roster queries so a delayed list cannot be mistaken for a newer one.

Broker lifecycle. The broker starts when needed and exits after five seconds with no registered sessions or federation links. Extension reload reuses a healthy broker; it does not replace the broker process. For broker-code updates, stop the broker gracefully and verify its exit before starting it again. Health reports the running PID, unique instance ID, package version and a source snapshot captured at startup. The source snapshot fingerprints packaged root/broker sources and metadata on disk, not dependency binaries or proof of module contents during concurrent checkout edits.

pi-parley vs pi-messenger

Aspect pi-parley pi-messenger
Model Targeted messaging, with explicit groups and opt-in broadcast Shared chat room
Primary use User orchestrating sessions Autonomous agent coordination
Discovery Broker-based (real-time) File-based registry
Messages Private by default; recipients are chosen for each send Broadcast to all agents
Persistence In Pi session history Shared coordination files

Pi-messenger centers a shared room; pi-parley centers conversations with chosen recipients. Its broadcast is host-local and still visibility-filtered.

pi-parley vs Threadroom

Parley is live conversation between working AI sessions. Threadroom is where an AI asks a person something and where discussions and decisions are kept after the sessions end. The two work well together without depending on each other.

  • A question for a person belongs in the person's question tool (Threadroom or ask_user_question), not in a Parley message to a session that happens to be near them.
  • If a Parley conversation produces something that should outlive the sessions in it, such as a decision, a discussion, or a work item, it belongs in Threadroom or the project's tracker. The Parley message can carry the link.
  • Parley retains message text in each session's history, but that is conversational context, not a shared record.

File Structure

<pi-parley package or checkout>/
├── package.json
├── index.ts              # Extension entry point
├── types.ts              # SessionInfo, Message, protocol types
├── config.ts             # Config loading
├── project-agent.ts      # Generic project-launch providers/commands and cwd target resolution
├── reply-tracker.ts      # Retained incoming context and explicit reply selection
├── conversation-history.ts # Journaled conversation recovery
├── message-results.ts    # Model-visible delivery and withdrawal outcomes
├── herdr/                # Session names in step with herdr's
│   ├── names.ts          # The naming rules and their lifecycle
│   └── socket.ts         # herdr's socket API: requests and pushed events
├── hosts/                # Parley inside Claude Code and Codex
│   ├── hosted-session.ts # The Parley extension on a stand-in for Pi's API
│   ├── serve.ts          # One host session's server, delivery and hooks
│   ├── claude.ts         # Claude Code: session id, inbox wake, hook output
│   ├── codex.ts          # Codex: thread from hooks, codex queue wake
│   ├── control.ts        # How hooks find their session's server
│   ├── mcp.ts            # A small MCP stdio server
│   ├── connect.ts        # parley connect|disconnect
│   └── cli.ts            # The parley command
├── link/                 # Linking computers over SSH
│   ├── keeper.ts         # This computer's link keeper process
│   ├── client.ts         # A session's connection to the keeper
│   ├── session.ts        # link/unlink tool actions and /parley link
│   ├── ssh.ts            # Broker access over the person's OpenSSH
│   ├── discover.ts       # Computers this one has ssh connections to
│   ├── protocol.ts       # Keeper requests, results and events
│   └── paths.ts          # Keeper state and control endpoint
├── broker/
│   ├── broker.ts         # Broker process and connection roles
│   ├── client.ts         # ParleyClient class
│   ├── build.ts          # Startup package/source identity
│   ├── process-lock.ts   # Kernel-owned lifetime leases
│   ├── runtime-claim.ts  # Broker ownership before initialization
│   ├── federation-types.ts    # Broker-peer wire contracts
│   ├── federation-protocol.ts # Strict validators and qualified ID codec
│   ├── federation-roster.ts   # Snapshot/delta import and resynchronization
│   ├── federation-send.ts     # Routed direct send frames and correlation
│   ├── federation-conversation.ts # Qualified identities and durable dispatch barriers
│   ├── attachment.ts          # Neutral owned streams and lazy provider registration
│   ├── federation-origin.ts   # Persisted canonical federation origin
│   ├── peer-link.ts      # Peer authority, handshake, and lifecycle
│   ├── framing.ts        # Length-prefixed JSON protocol
│   ├── paths.ts          # Platform-specific socket/pipe paths
│   ├── spawn.ts          # Auto-spawn and advisory startup ownership
│   ├── spawn.test.ts     # Broker spawn tests
│   └── paths.test.ts     # Path resolution tests
├── ui/
│   ├── session-list.ts   # Session selection overlay
│   ├── compose.ts        # Message composition overlay
│   └── inline-message.ts # Received message display
└── skills/
    └── pi-parley/
        └── SKILL.md      # Conversation and delivery context

Limitations

  • Remote conversations are negotiated text-only — Capable brokers and endpoint clients support asks/replies/progress; older links remain send-only or discovery-only. Attachments, remote mailboxes, and multi-hop routing are unsupported.
  • Bounded inspection, not a separate archive — pending and read expose retained incoming context; journaled history lives in the Pi session, not a standalone durable inbox
  • No attachments UI — file, snippet, and context attachments are supported in the protocol, but not in the compose overlay
  • Visibility is scoped — The roster includes connected sessions permitted by routing scope and child ACLs, not every Pi process or every sibling
  • Broker-memory mailboxes — The broker auto-spawns and clients reconnect after a restart, but queued offline messages do not survive broker exit

Updating Parley

The extension and the broker are separate processes. Reloading a session updates its extension, but reuses a healthy broker; the health handshake checks wire compatibility, not the installed package version. Rolling reloads and federation links can keep that broker running indefinitely.

For an update that changes broker code:

  1. Install or pull the desired release for every client sharing the runtime directory, then reload those clients. Do this at a quiet boundary: reconnecting interrupts blocking waits.
  2. On macOS/Linux, read broker.pid, verify it is the expected Parley process and send it SIGTERM; an unchecked PID can be stale or reused. Windows process signals force termination, so use the normal idle shutdown after disconnecting all clients and federation links when a graceful flush is needed. Verify broker exit before restart. Collaboration state is flushed on graceful shutdown; queued offline mail and broker-memory thread routes do not survive broker exit.
  3. Reconnect clients to start the broker from the updated installation. Check its new health instance ID/PID and startup package/source snapshot, then verify the roster and real messaging. The snapshot describes startup files, not dependency binaries or concurrently edited module contents. Federation controllers must reconnect their links.

There is no automatic version-based broker replacement. A client must not restart or downgrade a shared broker merely because its own installation differs.

Contributing

Bug reports and ideas are welcome in issues; CONTRIBUTING.md covers running the tests and what a change needs. Security problems go through private vulnerability reporting instead.

Credits

Parley began in March 2026 as a fork of pi-intercom by Nico Bailon. It has since grown into its own project: a broker built around conversations and delivery receipts, human-readable references, federation and linking computers over SSH. Nico's original copyright stays in LICENSE, and /intercom still opens Parley for anyone coming from pi-intercom. Thanks, Nico.

License

MIT. See LICENSE.