@ctliz/agent-intercom-pi
Pi coding-agent intercom for local messaging with Codex, Claude Code, OpenCode, Grok Build, and AGY agents.
Package details
Install @ctliz/agent-intercom-pi from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@ctliz/agent-intercom-pi- Package
@ctliz/agent-intercom-pi- Version
0.12.2- Published
- Sep 7, 2026
- Downloads
- 1,292/mo · 203/wk
- Author
- tsiji
- License
- AGPL-3.0-or-later
- Types
- extension, skill
- Size
- 571.1 KB
- Dependencies
- 6 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
],
"skills": [
"./skills"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
Pi Intercom
Agent Intercom is a cross-harness, same-machine messaging system for coding agents. Its Pi, Codex, Claude Code, OpenCode, Grok Build, and AGY adapters share one local broker and protocol, so sessions can discover and message each other regardless of which harness they run in.
| Harness | Repository |
|---|---|
| Core / Protocol | agent-intercom-core |
| Pi | agent-intercom-pi |
| Codex | agent-intercom-codex |
| Claude Code | agent-intercom-claude |
| OpenCode | agent-intercom-opencode |
| Grok Build | agent-intercom-grok |
| AGY | agent-intercom-agy |
| Fleet lifecycle | agent-intercom-orchestrator |
Grok Build and AGY support
Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
| Host | npm package | Installed MCP launcher |
|---|---|---|
| Grok Build | @ctliz/agent-intercom-grok |
agent-intercom-grok-mcp |
| AGY | @ctliz/agent-intercom-agy |
agent-intercom-agy-mcp |
The host packages depend on @ctliz/agent-intercom-claude and load its MCP runtime internally. Users do not need to install or place claude-intercom-mcp on PATH separately. Once connected, Grok and AGY sessions share the same local broker and protocol as Pi, Codex, Claude Code, and OpenCode, and expose the MCP operations: intercom_whoami, intercom_list, intercom_send, intercom_ask, intercom_reply, intercom_pending, intercom_status, intercom_team, intercom_join, and intercom_set_summary.
Install the host adapter before installing its plugin:
npm install -g @ctliz/agent-intercom-grok
npm install -g @ctliz/agent-intercom-agy
For multi-pane or Auto-Team-style use, the supervisor must give every MCP child a unique literal identity and the same scope as its intended peers:
AGENT_INTERCOM_SESSION_ID=<unique-pane-or-worker-id>
AGENT_INTERCOM_SESSION_NAME=<human-readable-name>
AGENT_INTERCOM_SCOPE_ID=<shared-team-or-workspace-scope>
The plugin manifests intentionally do not contain static session IDs, because sharing one ID across concurrent panes would create identity collisions. CLAUDE_INTERCOM_SESSION_ID and CLAUDE_INTERCOM_NAME remain higher-priority compatibility aliases.
Grok Build and AGY currently provide polling-only MCP integrations. Messages are durably retained, but there is no host-specific wake bridge to inject a new turn. Each active agent should call intercom_pending at startup and at natural work boundaries. They can join an existing team and exchange messages with every other adapter, but Agent Intercom Orchestrator does not currently spawn or lifecycle-manage Grok or AGY workers.
Maintenance & Upstream Provenance
- Maintained by
ctliz: This distribution is maintained independently by ctliz. - Upstream Heritage: Agent Intercom grew from Nico Bailon's original
pi-intercomand the upstreamdataforxyz/agent-intercom-*repositories. This project is not officially endorsed by or affiliated with upstream organizations. - Package Namespace: The canonical npm namespace is
@ctliz/*. The historical@dataforxyz/*namespace was used up to and includingconnect.1and is retained only as provenance and as a migration-detection input; it is never treated as a current or healthy installation. The Agent Intercom branding and theintercom_*API surface are unchanged.
Protocol v4 & Broker-Enforced Scope
Agent Intercom protocol v4 introduces broker-enforced scope routing via AGENT_INTERCOM_SCOPE_ID:
- Registration: The client submits its
scopeIdonce in the top-level registration payload. - Broker Enforcement: The shared local broker stores the scope in its private
ConnectedSessionrecord and enforces same-scope discovery (intercom_list), naming, and prefix matching. - Cross-Scope Routing: Cross-scope messaging is fail-closed; communication across different scopes is permitted only when addressing an explicit full session ID.
- UX Routing Isolation: Scope is designed for same-OS-user workflow isolation (e.g. per-project or per-workspace agent teams), not as a cryptographic security principal, tenant boundary, or authentication credential.
- Leak-Free: The raw
scopeIdvalue never entersSessionInfo, list payloads, lifecycle events, frontend displays, or execution logs. - Standalone First:
AGENT_INTERCOM_SCOPE_IDis a general shell/IDE/service launcher contract. Agent Intercom works completely standalone in any terminal, tmux window, or script; TmuxDeck is optional visual tooling.
Origin and thanks
Agent Intercom grew from Nico Bailon's original pi-intercom. A sincere thank you to Nico and the original contributors for creating the Pi extension and the foundation this cross-harness family builds on.
This repository contains the native Pi adapter. It provides direct 1:1 messaging, overlays, keyboard shortcuts, and agent tools while interoperating with Codex, Claude Code, and OpenCode sessions on the same machine.
User flow: press Alt+M or run /intercom to pick a session and send a message
Why
Sometimes you're running multiple pi sessions — one researching, one executing, one reviewing. Pi-intercom 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 and their current status
Unlike pi-messenger (a shared chat room for multi-agent swarms), pi-intercom is for targeted 1:1 communication where you pick the recipient.
Pi-intercom also integrates well with pi-subagents: delegated child agents get a child-only contact_supervisor tool when pi-subagents supplies bridge metadata. Use reason: "need_decision" for blocking clarification, reason: "interview_request" for multiple structured supervisor answers, and reason: "progress_update" for meaningful plan-changing updates. Normal sessions only see the regular intercom tool.
In One Minute
Each pi session that has pi-intercom loaded and enabled connects to a tiny local broker over a local IPC transport. The broker keeps track of connected sessions and routes direct messages to the one you target by name or session ID. The extension gives you both a tool (intercom) and a small overlay UI (/intercom 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
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.2
If you are coming from connect.1, read Upgrading from connect.1 first — the package namespace changed and the two versions must not be installed side by side.
Then restart Pi. The extension auto-connects to the broker on startup and registers the bundled pi-intercom skill for common coordination patterns.
On a supported Linux host (or WSL with a systemd user manager enabled) you can optionally install the orchestrator Pi plugin, which lets Pi create and safely own persistent Pi, Codex, Claude Code, and OpenCode coworkers. It is not required, and it is not available on macOS:
pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5
Restart Pi or run /reload, then verify:
agent_fleet({ action: "doctor" })
agent_fleet({ action: "capabilities" })
The orchestrator package adds the agent_fleet tool, /agents* commands, a scoped worker footer, and its manager Agent Skill. It uses exact systemd user-service cgroups, leases, adoption, and verified descendant cleanup. Linux with a working systemd user manager is required. Intercom handles communication; the orchestrator handles worker ownership and lifecycle, so most fleet-manager setups should install both packages.
Pi loads the Intercom extension directly, including its native Alt+I contact-copy shortcut, so no wrapper command or shell alias is required. You can still alias your usual Pi invocation for convenience, but unlike adapters that need a wrapper to add terminal behavior, an alias does not enable any additional pi-intercom features.
Pi-intercom is also protocol-compatible with the companion Codex, Claude, OpenCode, Grok Build, and AGY adapters. They share the same local broker and runtime directory, so sessions from all six hosts appear in the same session list and can send, ask, reply, and recover messages across host boundaries. The first connected adapter can start the broker; Pi does not need to be launched first.
Recommended: Add this snippet to your project's AGENTS.md to help agents understand when to coordinate across sessions:
<pi-intercom>
Coordinate with other local Agent Intercom sessions across Pi, Codex, Claude Code, and OpenCode. Use `/skill:pi-intercom` for patterns.
**When:** Same codebase (parallel work), reference codebase (consulting patterns), related repos (shared libraries).
**Not when:** Unrelated codebases, trivial questions, or when you can proceed independently.
**Principle:** Prefer `send` for notifications; `ask` only when blocked waiting for input.
</pi-intercom>
A session becomes intercom-connected when all of these are true:
- the
pi-intercomextension is installed and loaded in that session enabledis not set tofalsein the intercom config file, which defaults to~/.pi/agent/intercom/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 only shows intercom-connected sessions, not every open Pi process on the machine.
If you upgrade pi-intercom or the orchestrator while sessions are already open, run /reload in each open Pi session (and restart any companion coi, cci, or OpenCode adapter). Update the packages by reinstalling the exact release tags with pi install git:github.com/ctliz/agent-intercom-pi@v0.12.2 and, only where Orchestrator is actually installed, pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5. Extensions are loaded into the running host process, so an existing session cannot adopt new broker/discovery code until it reloads. This is especially important when upgrading from a release that allowed multiple broker processes to form separate session-list "islands": the broker ownership fix prevents new splits, but it cannot move clients that are still running the old code. After every host has reloaded once, they converge on the same broker automatically.
If /intercom still reports no peers, first confirm the other Pi windows have pi-intercom loaded and have also been reloaded. Open Pi processes without the extension, disabled sessions, and sessions using a different PI_CODING_AGENT_DIR intentionally do not appear in the same list.
If a session is unnamed, pi-intercom now exposes a runtime-only fallback alias like subagent-chat-1a2b3c4d so other sessions can still target it. That alias is not persisted as the Pi session title, so pi --resume can keep showing the transcript snippet instead of a generic session-... name.
Quick Start
Common Interface Across Hosts
The intercom packages use the same keyboard convention wherever the host exposes the required terminal hooks:
| Action | Pi | Codex (coi) |
Claude (cci/ccim) |
OpenCode |
|---|---|---|---|---|
| Pick a session and send | /intercom or Alt+M |
Alt+M | /claude-intercom:intercom or Alt+M |
/intercom or Alt+M |
| Copy this session's contact target | /intercom-id or Alt+I |
Alt+I | /claude-intercom:intercom-id or Alt+I |
/intercom-id or Alt+I |
Codex does not currently expose a native custom slash-command API, so its coi wrapper provides the shared keyboard shortcuts instead. Claude namespaces installed plugin commands and its terminal shortcuts require the attached cci or ccim wrapper. For those adapters, a shell alias is recommended because it makes the wrapper the normal launch command and ensures the shortcuts and wakeable intercom behavior are present. Pi loads those features natively, so an alias is optional here.
The contact text copied by Alt+I or /intercom-id is deliberately host-neutral. Paste it into any supported agent to identify the exact target without requiring that agent to look the session up by a machine-specific path or transient display label.
From the Keyboard
Press Alt+M or type /intercom to open the session list overlay:
- Select a session — Use arrow keys to pick a target session
- Find and select — Start typing to filter long session lists
- Compose message — Write or paste a multiline message; use Shift+Enter for a newline
- Send — Press Enter to send, Escape to cancel
Press Alt+I or run /intercom-id to copy a short handoff snippet for the current session. The snippet uses the session's unique name when possible, falling back to the stable intercom session ID when names are duplicated.
From the Agent
The agent uses eight focused tools: intercom_send, intercom_ask, intercom_reply, intercom_team, intercom_join, intercom_list, intercom_pending, and intercom_status. Tool calls and results render as compact transcript rows so coordination is easy to scan. For common patterns like planner-worker delegation, the bundled pi-intercom skill provides copy-paste ready examples:
// Find the manager and managed coworkers without searching the global list
intercom_team({})
// → Manager: manager-id [connected]
// → You: reviewer
// → Coworkers: builder target=builder (codex, builder, running) [connected]
// List all active sessions
intercom_list({})
// → **Current session:**
// → • executor (20d43841) — ~/projects/api (claude-sonnet-4) [self, idle]
// → **Other sessions:**
// → • research (6332faab) — ~/projects/api (claude-sonnet-4) [same cwd, thinking]
// Send a message
intercom_send({ to: "research", message: "Check if UserService.validate() handles null" })
// → Message sent to research
// Check connection status
intercom_status({})
// → Connected: Yes, Session ID: abc123, Active sessions: 3
// Send with attachments (code snippets, files, or context)
intercom_send({
to: "worker",
message: "Here's the fix:",
attachments: [{
type: "snippet",
name: "auth.ts",
language: "typescript",
content: "function validate(user: User) { ... }"
}]
})
Receiving Messages
When a message arrives, it appears inline in your chat with the sender's info and a reply hint:
**From research** (~/projects/api)
To reply, use the intercom tool: intercom_reply({ message: "..." })
Found the issue — UserService.validate() doesn't check for null input.
See auth.ts:142-156.
The reply hint (enabled by default) points to intercom_reply({ ... }), so recipients never need protocol thread IDs. Incoming messages are first written to a durable per-session inbox. Messages arriving within a 300 ms quiet window are combined into one model turn, with a 1-second maximum batching delay so a steady stream cannot postpone delivery forever. Busy sessions keep the batch queued until they are idle. Every original sender, message ID, timestamp, attachment, and reply context remains available in the batch details.
The sender receives two distinct delivery states in structured tool details: accepted means the broker accepted the message for routing, while delivered means the receiver durably queued it and acknowledged the delivery. Outbound messages are also written to a durable per-session outbox before transmission. If the broker disconnects between acceptance and receiver acknowledgement, the next connection automatically replays the original target, payload, and message ID. Attachment content is included in the agent-visible body, and delivered messages are rendered inline and stored in Pi session history.
Structured extension controls
Trusted companion extensions can register structured control types that bypass ordinary model-message injection. Pi Intercom remains only the transport: it validates the bounded control envelope, durably enqueues and acknowledges it, consumes it before dispatch, and emits the broker-verified sender session ID through Pi's in-process event bus. The companion extension owns authorization and the privileged action.
Event contract:
| Event | Direction | Payload |
|---|---|---|
intercom:control:register |
companion → Intercom | { type, version } |
intercom:control:send |
companion → Intercom | { requestId, to, control, fallbackText?, messageId? } |
intercom:control |
Intercom → companion | { from, messageId, receivedAt, control } |
intercom:control:delivery |
Intercom → companion | { requestId, delivered, targetSessionId?, messageId?, deliveryId?, code?, error? } |
A control envelope is { type: string, version: positiveInteger, data?: unknown }, with a 128-character type limit and a 16 KiB JSON data limit. Register types at extension factory time when possible and again during session_start to cover either extension load order. Recovered structured controls receive a short 250 ms registration grace before an unknown type falls back to ordinary message delivery.
Registered controls are consumed in the durable inbox before intercom:control is emitted. This prevents reload/reconnect loops when the receiving action tears down the current extension runtime. Control dispatch is therefore intentionally at most once after durable receipt: if a companion handler throws after consumption, Pi Intercom records an intercom_control_handler_error custom entry but does not replay the privileged action. Companion protocols that need an application-level completion guarantee must send their own result control. Unknown or unregistered controls follow the ordinary message path and retain their plain-text compatibility fallback instead of being silently discarded.
Sender identity in intercom:control.from.id comes from the broker delivery frame. Names, CWD, model, PID, and control data remain untrusted metadata. A companion performing a privileged action must authorize the stable sender session ID against its own current policy.
The control bus is intentionally not exposed as a model-facing generic Intercom tool. Companion extensions expose only their specific, reviewed actions.
Teams, names, and who can talk to whom
A team in Agent Intercom is the small group a session should treat as coworkers. It is not the same as the machine-wide session list. Agents that belong to a team should start with intercom_team({}) and only fall back to intercom_list({}) when they need an independently launched peer.
/name is how a human gives a session a stable, speakable handle. The broker still identifies every session by a stable session ID; the name is the convenient address other agents should use when it is unique.
Name a session with /name
/name is a Pi host command, not an Intercom command. After you set it, Intercom republishes the new name so other connected sessions can target it.
# Terminal 1
/name planner
# Terminal 2
/name worker
# Terminal 3
/name reviewer
Good names are short, unique in the same scope, and describe the role: planner, api-worker, frontend-dev. Avoid spaces. If two live sessions share the same name, prefer the short ID shown by intercom_list or the exact full session ID from /intercom-id.
Check the published name:
intercom_status({})
intercom_list({})
Copy a host-neutral contact snippet for another agent:
/intercom-id
That snippet uses the unique name when possible and falls back to the stable session ID when names collide.
If you never set /name, Intercom still exposes a runtime-only fallback alias such as subagent-chat-1a2b3c4d so peers can reach the session. That alias is not stored as the Pi session title.
How intercom_team chooses a team
intercom_team({}) has no arguments. It resolves the current group in this order and stops at the first match:
- Orchestrator —
~/.pi/agent/intercom/orchestrator/workers.jsonhas an owned record for this session (AGENT_INTERCOM_WORKER_ID) or this session is the current manager of live owned coworkers. - TmuxDeck manifest —
AGENT_INTERCOM_TEAM_MANIFESTpoints at a valid team file. The Lead isleadId; workers are the other members. An invalid or empty manifest fails closed and does not fall through to the live roster. - Same-scope live roster —
AGENT_INTERCOM_SCOPE_IDis set. Coworkers are the other live non-human sessions in that scope.AGENT_INTERCOM_MANAGER_TARGET/AGENT_INTERCOM_MANAGER_SESSION_IDname the Lead when present. - Standalone — no manager and no coworkers. Create a named team with
/intercom-createorintercom_join({ create: true }), or use/nameplusintercom_list/intercom_send.
Typical result:
Manager: planner [connected]
You: worker
Coworkers:
- reviewer target=reviewer (pi, worker, running) [connected]
Use the returned target with intercom_send and intercom_ask. After Orchestrator adoption, the manager target updates without restarting the worker. AGENT_INTERCOM_MANAGER_TARGET is only a startup fallback.
Only an Orchestrator manager may inspect another session's pending inbox, and only with an exact connected coworker target from intercom_team:
intercom_team({})
intercom_pending({ session: "tmuxdeck-11111111-2222-4333-8444-555555555555" })
Manifest, live-roster, and standalone teams cannot inspect another session's inbox.
Create or join a team without tmux
Named teams live in the local Intercom directory. Creating one generates a private scope, makes this session the manager, and is enough for intercom_team to return a live roster. Tmux and TmuxDeck are not required.
/intercom-create billing # create a named team and join as manager
/intercom-join billing # join that named team from another session
/intercom-join # list named teams and TmuxDeck workspaces
/intercom-join 1 # join by the listed number
/intercom-status # confirm membership and visible peers
intercom_team({})
Agents can do the same without a slash command:
intercom_join({ name: "billing", create: true }) // create and join as manager
intercom_join({ name: "billing" }) // join an existing named team
intercom_join({}) // list joinable teams
/intercom-join can still attach a standalone Pi session to an existing TmuxDeck workspace circle. That path is same-scope messaging only. It does not write a team manifest, does not enroll you as a Team Worker, and does not grant inbox inspection.
/intercom-join frontend # join by exact TmuxDeck workspace name
/intercom-join --scope <48hex> # join by the workspace scope
Managed Team Workers and Orchestrator-owned sessions cannot join another circle. Listing never prints the raw scope.
Example: two-person billing team
Open two terminals in the same repo and start Pi in each.
# Terminal 1 — the person holding the plan
/name planner
intercom_status({})
# Terminal 2 — the person making the change
/name worker
intercom_list({})
intercom_list should now show both planner and worker. They are still standalone until one session creates a named team:
# Terminal 1
/intercom-create billing
# Terminal 2
/intercom-join billing
intercom_team({})
After that, intercom_team returns the pair as a live roster. If these terminals were launched inside the same TmuxDeck workspace, the live roster is already present without /intercom-create.
Planner assigns work:
intercom_send({
to: "worker",
message: "Add idempotent refund retries in src/billing/refunds.ts. Ask before changing the public error shape."
})
Worker asks a blocking question, then reports done with send:
intercom_ask({
to: "planner",
message: "Refunds currently return 500 on a mid-flight timeout. Should I map that to 409 Conflict?"
})
// planner replies in the triggered turn:
intercom_reply({ message: "Yes. Keep the existing error body; only change the status code." })
intercom_send({
to: "planner",
message: "Done. Timeouts now return 409. Tests in refunds.test.ts pass."
})
If a third Pi is already running as reviewer, join the named team from a new standalone window:
/intercom-join billing
/name reviewer
intercom_team({})
intercom_send({ to: "planner", message: "I can review the refund retry once worker is done." })
Workflow: Planner-Worker Coordination
The most natural use of pi-intercom is splitting a task between two sessions — one holds the big picture, the other does the hands-on work. When the worker hits an ambiguity ("should I optimize for readability or performance here?"), they ask without losing context.
Setup
Open two terminals and start pi in each. Name them so they can find each other:
# Terminal 1 # Terminal 2
/name planner /name worker
Verify they see each other from either session:
intercom_list({})
// → • worker — ~/projects/api (claude-sonnet-4) [idle]
The Conversation
Here's how a typical exchange looks. The planner delegates with send (fire-and-forget). The worker uses ask for anything that needs a response — questions, discoveries, completion reports. ask waits up to 30 seconds so a prompt answer can return in the same tool result, then releases the worker and delivers any late answer as a new intercom message.
Planner sends a task:
intercom_send({
to: "worker",
message: "Task-3: Add retry logic to API client. Key files: src/api/client.ts, src/api/types.ts. Ask if anything's unclear."
})
Worker hits an ambiguity — asks and waits:
intercom_ask({
to: "planner",
message: "Should retry apply to all endpoints or just idempotent ones? Also, max retry count and backoff strategy?"
})
// → Reply from planner: Only GET/PUT/DELETE — never POST. Max 3 retries, exponential backoff starting at 100ms.
// Worker continues implementing with the answer, same turn, full context.
Worker finds something unexpected — escalates and waits:
intercom_ask({
to: "planner",
message: "Found: fetchWithTimeout swallows network errors. Fixing this changes the error shape. OK to proceed?"
})
// → Reply from planner: Yes, surface the error types. The current behavior is a bug.
Worker reports completion:
intercom_ask({
to: "planner",
message: "Task-3 done. Added RetryPolicy type, applied to GET/PUT/DELETE, surfaced NetworkError, 4 tests passing."
})
// → Reply from planner: Looks good. Move on to task-4.
Communication Patterns
| Pattern | Action | Why |
|---|---|---|
| Task Delegation | Planner uses send |
Fire-and-forget. Planner doesn't need to wait for an ack. |
| Clarification Request | Worker uses ask |
Worker needs the answer to proceed. Blocks until reply. |
| Discovery Escalation | Worker uses ask |
Worker needs approval before changing course. |
| Completion Report | Worker uses ask |
Planner might have follow-up instructions or the next task. |
Reply Hints
When replyHint is enabled (the default), incoming messages include the exact intercom_reply() call to respond:
**From planner** (~/projects/api)
To reply, use the intercom tool: intercom_reply({ message: "..." })
Only GET/PUT/DELETE — never POST. Max 3 retries with exponential backoff starting at 100ms.
This matters because the agent receiving the message never needs to see or reconstruct protocol thread IDs. Combined with idle-gated triggerTurn delivery, it enables real back-and-forth conversation without interrupting work in progress. If the reply happens later instead of in the triggered turn, intercom_reply({ message: "..." }) falls back to the single unresolved inbound ask, and intercom_pending({}) shows who is still waiting with stable receiver-local ask IDs.
send vs ask
intercom_send is non-blocking with respect to a reply: it waits only for the receiver's durable-enqueue acknowledgement, then returns. By default, it sends immediately even in interactive sessions. If you want an approval dialog before non-reply sends, set confirmSend: true in config. intercom_reply skips confirmation so reply-hint flows can continue without an extra approval step.
intercom_ask sends a genuinely blocking question and waits up to 30 seconds for the recipient. A prompt reply comes back as the tool result, so the agent continues in the same turn with full context. If nobody replies within 30 seconds, the tool returns control without an error and keeps the request open asynchronously; a late reply arrives as a new intercom message. Different recipients may be asked concurrently, but only one unresolved ask per recipient is allowed. Use intercom_send for assignments, progress/status checkpoints, notifications, and follow-ups.
intercom_reply is receiver-side sugar for replying to an inbound ask. In the turn triggered by an incoming intercom ask, intercom_reply({ message: "..." }) targets that exact sender and message automatically. If you reply later, it falls back to the single unresolved inbound ask. If multiple asks are pending, use the stable ID from intercom_pending({}) with intercom_reply({ askId, message: "..." }); the older to plus which selectors remain supported.
The planner typically uses send. If you prefer manual approval for outgoing non-reply messages, turn on confirmSend: true. Workers should also use send for progress and completion, reserving ask for decisions they cannot safely continue without.
Workflow: Subagent-to-Supervisor Escalation
This workflow requires pi-subagents to be installed and to supply child bridge metadata. When pi-subagents spawns a delegated child with that metadata, the child session gets a subagent-only contact_supervisor tool in addition to the regular intercom tool. Normal sessions never see contact_supervisor.
When the Tool Appears
contact_supervisor only registers when pi-subagents sets all of these environment variables:
PI_SUBAGENT_ORCHESTRATOR_TARGET— the supervisor session name or IDPI_SUBAGENT_RUN_ID— the run identifierPI_SUBAGENT_CHILD_AGENT— the agent typePI_SUBAGENT_CHILD_INDEX— the child index within the run
If any are missing, the session falls back to the regular intercom tool.
Three Reasons
| Reason | Behavior | Use When |
|---|---|---|
need_decision |
Waits up to 30 seconds, then continues asynchronously if unanswered | The subagent is blocked, uncertain, needs approval, or faces a product/API/scope decision |
interview_request |
Waits up to 30 seconds for structured answers, then continues asynchronously | The subagent needs multiple machine-readable answers from the supervisor in one exchange |
progress_update |
Fire-and-forget update to the supervisor | Meaningful progress or unexpected discoveries that change the plan |
Do not use contact_supervisor for routine completion handoffs. Return the final subagent result normally through pi-subagents.
Example: Blocked Subagent Asks for Guidance
contact_supervisor({
reason: "need_decision",
message: "The auth service returns 403 instead of 401 for expired tokens. Should I treat 403 as a re-auth trigger or a hard failure?"
})
// → Reply from supervisor: Treat 403 as re-auth trigger. Update the token refresh logic.
Example: Structured Supervisor Interview
contact_supervisor({
reason: "interview_request",
message: "Please answer these before I continue the migration.",
interview: {
title: "API migration choices",
questions: [
{ id: "api", type: "single", question: "Which API should I target?", options: ["Stable API", "Experimental API"] },
{ id: "constraints", type: "text", question: "What constraints should I preserve?" }
]
}
})
// → Reply from supervisor: { "responses": [{ "id": "api", "value": "Stable API" }, ...] }
Example: Progress Update
contact_supervisor({
reason: "progress_update",
message: "Discovered the bug is in the retry wrapper, not the API client. Fixing the wrapper will also close issue #42."
})
// → Progress update sent to supervisor planner
What the Supervisor Sees
The supervisor receives a formatted message with run metadata:
**From subagent-worker-78f659a3-1**
Subagent needs a supervisor decision.
Run: 78f659a3
Agent: worker
Child index: 0
Which API should I use?
Reply hints work the same as regular intercom_ask/intercom_reply flows. The supervisor can reply with intercom_reply({ message: "..." }) and the subagent receives the answer as the tool result.
For interview_request, the supervisor message includes the structured questions plus a fenced JSON answer example using this stable shape:
{
"responses": [
{ "id": "api", "value": "Stable API" },
{ "id": "constraints", "value": "Keep the public error shape unchanged." }
]
}
The supervisor can reply with plain JSON or a fenced json block. If the reply matches the { "responses": [...] } shape and references valid question ids/options, the child tool result includes it in details.structuredReply while still showing the raw reply text.
Tool Reference
Split intercom tools
| Tool | Parameters | Description |
|---|---|---|
intercom_send |
required to, required message, optional attachments |
Fire-and-forget delivery |
intercom_ask |
required to, required message, optional attachments |
Ask and wait briefly for a reply |
intercom_reply |
required message, optional askId, to, which |
Reply to the active or pending inbound message; askId selects an exact unresolved ask, while to/which remain compatible selectors |
intercom_team |
none | Show the current manager and live coworkers owned by that manager |
intercom_join |
optional name, optional create |
List, join, or create a named team without tmux |
intercom_list |
none | List connected sessions in your scope |
intercom_pending |
optional askId, session |
List unresolved inbound asks with stable IDs; askId retrieves the full untruncated body, and managers may use session for an owned coworker |
intercom_status |
none | Show connection and queue status |
Exact protocol threading is internal. Stable askId values are receiver-local selectors, not wire message/thread IDs; no split tool exposes replyTo or reply_to.
contact_supervisor
Only registered in sessions where pi-subagents supplied the required child bridge metadata. Contacts the supervisor session that delegated the current task.
| Parameter | Type | Description |
|---|---|---|
reason |
string | "need_decision" (soft-waiting), "interview_request" (soft-waiting 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 waits up to 30 seconds. A prompt reply comes back as the tool result; otherwise the request remains open and a late reply arrives asynchronously. 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 waits up to 30 seconds. 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": ... }] }. Prompt parsed replies are returned in details.structuredReply; late replies arrive asynchronously.
progress_update — Sends a non-blocking update to the supervisor. Returns immediately after delivery. Use only for meaningful progress or unexpected discoveries that change the plan.
Tool behavior
intercom_team reads orchestrator ownership dynamically and returns the current manager plus live same-manager coworkers. After adoption it follows the new manager without restarting the worker; AGENT_INTERCOM_MANAGER_TARGET is only a startup fallback.
intercom_join lists named teams and TmuxDeck workspaces, joins one by name, or creates a named team with create: true. Creating a team does not require tmux. Listing never prints the raw scope.
intercom_list returns the current session plus other active intercom-connected sessions with name, short ID, working directory, model, and live status. Under protocol v4 it is same-scope: it returns only sessions sharing your scope (or only unscoped sessions when you are unscoped). Cross-scope contact is possible only by exact full session ID. Orchestrator-owned workers should still prefer intercom_team for their group.
intercom_send sends immediately and distinguishes broker accepted from receiver-acknowledged delivered. Set confirmSend: true for an interactive confirmation dialog.
intercom_ask waits up to 30 seconds for a prompt reply, then returns a successful pending result while keeping the request open for a late reply. Different recipients may wait concurrently; the same recipient may have only one unresolved ask. PI_INTERCOM_ASK_WAIT_MS changes the blocking window.
intercom_reply resolves the active or pending inbound context internally. Pass the stable askId returned by intercom_pending to select one exact ask. Optional to and which: "oldest" | "latest" remain available for compatibility. None of these values exposes the protocol thread ID.
The broker refuses a second unresolved intercom_ask from one session to the same recipient. Wait for the first answer or use intercom_send for a non-blocking follow-up.
intercom_pending lists unresolved inbound asks waiting for this session's reply, with a stable ask ID, sender, elapsed time, and the existing 80-character preview. Pass askId to retrieve that ask's full untruncated body and attachments. A manager may also pass an exact connected stable session matching an owned local coworker's target from intercom_team; worker-record IDs, names, prefixes, offline targets, workers, remote peers, and unrelated sessions fail closed. Foreign inbox inspection is read-only and refuses version skew or corruption without migrating, rewriting, or quarantining the target file. It does not list outbound asks sent to coworkers.
intercom_status shows connection, session, queue, and pending-ask status.
The deprecated monolithic intercom({ action: ... }) tool is hidden by default. Enable legacyTool only for temporary compatibility.
Trusted-local Boss team scope
Boss-launched Pi sessions can provide this advisory environment contract:
| Variable | Value |
|---|---|
AGENT_INTERCOM_BOSS_RUN_ID |
Canonical lowercase boss-<UUID> run identifier |
AGENT_INTERCOM_BOSS_ROLE |
manager, worker, scout, or adversary |
AGENT_INTERCOM_BOSS_CONTROLLER_TARGET |
Exact stable Controller session ID |
AGENT_INTERCOM_BOSS_MANAGER_TARGET |
Canonical boss-manager-<run-suffix> session ID |
AGENT_INTERCOM_BOSS_TEAM_TARGETS |
JSON array containing exactly the canonical Manager, Worker, Scout, and prospective Adversary session IDs |
AGENT_INTERCOM_BOSS_VISIBILITY |
team-only or local; defaults to team-only when Boss metadata is present |
In team-only mode, intercom_team shows only live canonical teammates. A Manager also sees and may contact the exact creating Controller; worker, scout, and adversary roles cannot contact that Controller. The sole lower-role exception is the exact versioned Orchestrator readiness probe/ack control handshake with that Controller; it is consumed as registered extension control, never exposed to the model or ordinary inbox/reply paths, and does not authorize any other control or message. Same-scope intercom_list discovery is not registered, the legacy list action is denied, and /intercom / Alt+M show the same permitted live set. Outbound sends, asks, replies, overlay delivery, and inbound messages use exact stable session IDs only: names, case folding, and ID prefixes are rejected. Inbound policy is applied before inbox persistence, reply tracking, activity events, or model triggering. Boss sessions discard persisted outbound messages instead of replaying them after reconnect, because broker replay cannot independently prove that a stored string still denotes the intended exact live ID. The prospective Adversary appears only after its canonical exact ID connects.
The contract is all-or-nothing: if any Boss variable is present but metadata is incomplete, malformed, noncanonical, or inconsistent with the connected session's declared role, discovery and delivery fail closed. AGENT_INTERCOM_BOSS_VISIBILITY=local broadens discovery and communication to local sessions but still requires exact stable IDs. TRUSTED LOCAL MODE — same-user agents and local files are trusted; evidence is advisory, not tamper-proof. Team metadata is advisory trusted-local scoping, not broker-enforced hostile-agent isolation.
Keyboard Shortcuts
| Key | Action |
|---|---|
| Alt+M | Open session list overlay |
| Alt+I | Copy this session's intercom contact target, falling back to editor insert |
| ↑/↓ | Navigate session list |
| Enter | Select session / Send message |
| Shift+Enter | Insert a newline while composing |
| Escape | Cancel / Close overlay |
The session picker is searchable, multiline bracketed paste is preserved, and displayed session metadata is sanitized before it reaches the terminal.
Config
Create ~/.pi/agent/intercom/config.json:
{
"brokerCommand": "npx",
"brokerArgs": ["--no-install", "tsx"],
"confirmSend": false,
"inboundTrigger": "always",
"enabled": true,
"replyHint": true,
"legacyTool": false,
"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 non-reply sends from an interactive session with UI |
inboundTrigger |
"always" |
Auto-trigger policy for inbound broker messages: "always", "replies", or "never". Local in-process subagent relay events still trigger the addressed session. |
enabled |
true | Enable/disable intercom entirely |
replyHint |
true | Include reply instruction in incoming messages |
legacyTool |
false | Expose the deprecated monolithic intercom({ action: ... }) tool. Compatibility only; can also be enabled with PI_INTERCOM_LEGACY_TOOL=1, and scheduled for removal at the next major boundary. |
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-intercom logs the error and fails closed for inbound broker auto-triggering by using inboundTrigger: "never" until the config is fixed.
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-intercom publishes live session status automatically. Sessions register as idle, switch to thinking while the agent is running, show tool:<name> during tool execution, and return to idle on agent completion. If status is set in config, it is appended as context instead of replacing the lifecycle status.
By default, runtime state and config live under ~/.pi/agent/intercom. If Pi is launched with PI_CODING_AGENT_DIR, pi-intercom uses $PI_CODING_AGENT_DIR/intercom instead, including config.json, broker PID/lock files, sockets, durable inboxes/outboxes, ask state, and launcher state. PI_INTERCOM_ASK_WAIT_MS controls the foreground ask wait (30 seconds by default); PI_INTERCOM_ASK_TIMEOUT_MS controls how long a deferred ask remains replyable (10 minutes by default).
How It Works
graph TB
subgraph A["Pi Session A"]
A1[Intercom Client]
A2[intercom tool]
A3[UI overlays]
end
subgraph Broker["Intercom Broker"]
B1[Session Registry]
B2[Message Router]
end
subgraph B["Pi Session B"]
B3[Intercom Client]
B4[intercom 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 intercom-enabled session needs it and exits after 5 seconds when the last connected session disconnects. Clients now reconnect automatically if the broker disappears and later comes back.
Messages use strict pi-intercom protocol v3 over length-prefixed JSON on a local socket/pipe transport (4-byte length + JSON payload). Registration rejects incompatible protocol versions instead of attempting a partially compatible connection. The protocol includes request correlation, structured error codes, delivery IDs, receiver acknowledgements and rejections, retry deduplication by sender session plus message ID, acknowledged ask controls, payload and pending-work bounds, a frame-size cap, byte-weighted per-connection rate limiting, and no-op presence coalescing.
The receiver acknowledges only after atomically writing the message to its per-session inbox. Delivery is therefore at least once across reconnects and reloads. There is one narrow crash window after a batch is injected into Pi but before its inbox entries are marked consumed; after recovery, that batch can be shown again rather than silently lost.
Session IDs are the trusted addressing key inside the broker, but local Intercom is still a same-UID trust boundary rather than a cryptographic principal boundary. Duplicate names remain allowed for same-user workflows, but sends to ambiguous names fail and users should target the stable session ID shown by list/status in trust-sensitive flows. Reconnects from one live runtime carry an ephemeral runtime instance ID and may replace that runtime's stale socket. A different live runtime claiming the same stable session ID receives SESSION_ID_IN_USE, leaving the original connection and its pending deliveries intact; legacy adapters without a runtime instance ID fall back to matching PID plus runtime start time. This prevents accidental reconnect wars when, for example, desktop Pi and a mobile RPC host open the same transcript simultaneously. It is not authentication against a malicious same-UID process, which can still spoof local registration metadata. Consequently, structured-control consumers must treat authorization by sender session ID as protection against accidental or unrelated peer actions—not as protection from a malicious process already running as the same OS user. 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/intercom/ by default, or $PI_CODING_AGENT_DIR/intercom/ 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 windowbroker.pid— Broker process IDbroker.owner— Lifetime ownership record that prevents another live broker from replacing the active socketbroker.spawn.lock— Auto-spawn lock filebroker.port.json— Dynamic localhost TCP endpoint, only when Windows TCP transport is explicitly enabledbroker-asks.json— Expiring ask/reply authorization edges; restored as deferred after broker restartconfig.json— User configurationinbox/<session-hash>.json— Durable ordered inbound messages and receiver-side deduplication stateoutbox/<session-hash>.json— Durable unfinished outbound messages replayed after reconnect
Design Decisions
Local IPC instead of TCP. Same-machine only by design. pi-intercom uses Unix sockets on macOS/Linux and a named pipe on Windows, which keeps setup simple and avoids port management. Windows TCP is available only as an explicit escape hatch with PI_INTERCOM_TRANSPORT=tcp (or PI_INTERCOM_TCP=1) for environments where named pipes are blocked. In that mode the broker binds a dynamic 127.0.0.1 port, records the endpoint plus a local secret under the intercom 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.
Auto-spawn with two levels of ownership. The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A short-lived spawn lock, keyed by PID and timestamp, coordinates clients that try to start the broker at the same time. A separate lifetime ownership record prevents any second live broker process from unlinking or replacing the active socket, including brokers launched manually or by different host adapters.
ask has a soft foreground wait. The client waits up to 30 seconds for a matching reply and returns a prompt reply as the tool result. After that soft wait expires, the sender continues and asks the broker to change the edge from blocking to deferred. That control operation is explicitly acknowledged. Deferred asks permit reverse asks, remain late-replyable until the 10-minute expiry, and survive broker restart without recreating a blocking dependency. Reply hints make the flow practical by preserving the exact reply context.
pi-intercom vs pi-messenger
| Aspect | pi-intercom | pi-messenger |
|---|---|---|
| Model | Direct 1:1 messaging | Shared chat room |
| Primary use | User orchestrating sessions | Autonomous agent coordination |
| Discovery | Broker-based (real-time) | File-based registry |
| Messages | Private, session-to-session | Broadcast to all agents |
| Persistence | Durable delivery inbox plus Pi session history | Shared coordination files |
Use pi-messenger for multi-agent swarms working on a shared task. Use pi-intercom when you want to manually coordinate your own sessions or have one agent reach out to another specific session.
File Structure
~/.pi/agent/extensions/pi-intercom/
├── package.json
├── index.ts # Extension entry point
├── types.ts # SessionInfo, Message, protocol types
├── control.ts # Bounded structured-control envelopes and event contracts
├── config.ts # Config loading
├── durable-json.ts # Atomic fsync-backed JSON persistence helper
├── inbound-inbox.ts # Durable inbound queue and deduplication
├── outbound-outbox.ts # Durable sender replay queue
├── broker/
│ ├── broker.ts # Broker process
│ ├── client.ts # IntercomClient class
│ ├── framing.ts # Length-prefixed JSON protocol
│ ├── paths.ts # Platform-specific socket/pipe paths
│ ├── spawn.ts # Auto-spawn logic with lock file
│ ├── 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
│ └── session-identity.ts # Unique prefixes and safe display metadata
└── skills/
└── pi-intercom/
└── SKILL.md # Bundled skill for common patterns
Compatibility, Migration & Rollback
- Single Shared Broker: The broker-capable adapters on the machine —
pi,claude,codex, andopencode— connect to one local broker over a Unix domain socket (~/.pi/agent/intercom/broker.sockor$PI_CODING_AGENT_DIR/intercom/broker.sock). - Coordinated Upgrade Set: Protocol v4 changes broker negotiation, so the broker-capable adapters that are actually installed and enabled on this machine must be upgraded together in one maintenance window. Adapters you do not use do not need to be installed to satisfy the upgrade.
@ctliz/agent-intercom-coreis an internal dependency that arrives with the adapters and is never installed or upgraded on its own. - Orchestrator is Optional:
agent-intercom-orchestratoris an optional Linux/systemd lifecycle component. It does not implement or start a Broker and is not part of the Broker compatibility set. Omitting it — for example on macOS, or when using TmuxDeck — is a fully supported configuration and is not a mixed or unsupported state. If it is installed on a supported Linux host, or on WSL with a systemd user manager enabled, update it together with the adapters it manages so scope inheritance and package identity stay aligned. - Fail-Closed Legacy Handling: An incompatible legacy (v3) broker or client fails closed. It is rejected at negotiation and never killed, never downgraded, and never allowed to form a second broker island.
- Rollback: Rolling back covers only the components that were actually installed on this machine before the upgrade. Restore the exact specs and lockfiles you backed up, then reload the affected agent sessions. Roll Orchestrator back only if it was installed to begin with. There is no published pre-v4 tag under
ctliz, so a pre-upgrade backup of the exact installed specs/locks is the supported rollback material. Leaving some installed broker-capable adapters on the old protocol while others are upgraded is an unsupported mixed state.
Upgrading from connect.1 to connect.2
connect.2 renames the package namespace from @dataforxyz/* to @ctliz/*. The two namespaces are different packages to npm. Pi Git package installations deduplicate by repository URL without ref, but running agent sessions continue to execute legacy code in memory, and npm or global installs along with binary links can coexist and conflict. Operators must stop active sessions, clean active install surfaces, and follow remove-before-install — side-by-side installation is not supported.
- Back up the exact specs, lock files, and settings of every installed component.
- Stop or close the installed broker-capable adapters.
- Remove the old
@dataforxyz/*specs, packages, and binary links that are actually installed. - Assert the old identity is gone from the active install surfaces of the current OS user: Pi settings and extension specs, resolved managed install roots, actual
node_modulesinstallations, and conflicting binary links that the currentPATHwould resolve. Do not scan or delete unrelated source checkouts, historical documentation, or other users' files — a@dataforxyz/*string in an unrelated development clone is not an installation. - Install the
@ctliz/*connect.2exact tags for the components you actually use. - Run
/reloador restart, then verify exactly one broker is running.
Classification rule. Migration-aware connect.2 setup and update tooling must classify an old-namespace-only install surface as MIGRATION_REQUIRED, and the simultaneous presence of both namespaces as a duplicate/dual-load hard error that refuses setup, update, and further installation. This tooling does not exist for every platform and adapter combination; where it is not available, apply the same two rules manually against the surfaces in step 4. Do not assume every adapter emits this code automatically.
Rollback reverses this and covers only the components that were installed on this machine before the upgrade: remove the @ctliz/* packages, then restore the backed-up exact @dataforxyz/* specs and locks. Roll Orchestrator back only if it was installed to begin with.
The connect.1 tags, source commits, and published release assets are immutable and are not modified by this migration. Release notes may carry an explicit erratum, which corrects the description only and never moves a tag or replaces an asset.
These packages are not published on the npm registry yet; install from the GitHub tags shown above.
Limitations
- Same machine only — Uses local sockets/pipes, no network support
- No separate transcript UI — Messages are kept in Pi session history and a durable delivery inbox, but there is no standalone inbox browser
- No attachments UI —
file,snippet, andcontextattachments are supported in the protocol, but not in the compose overlay - Only connected sessions appear — The list shows Pi sessions that have loaded
pi-intercomand successfully registered with the broker, not every open Pi process on the machine - Broker lifecycle — The broker auto-spawns on first use and exits when idle; sessions reconnect automatically if the broker restarts
- At-least-once recovery — A crash in the small interval between Pi injection and inbox consumption can replay a batch after restart
- Bounded sender queue — Each session keeps at most 256 unfinished outbound messages; definitive delivery failures are removed rather than retried forever
Releasing
Releases are automated from version tags. Update package.json, the lockfile when
present, and CHANGELOG.md on main, then push an annotated tag that exactly
matches the package version:
git tag -a vX.Y.Z -m "vX.Y.Z"
git push origin vX.Y.Z
The release workflow verifies that the tag points into main, runs typecheck and
tests, publishes the public npm package with trusted OIDC provenance, and creates
the GitHub Release. Existing npm versions and GitHub Releases are skipped safely
when a workflow is rerun.
License
The current project is licensed under the GNU Affero General Public License
v3.0 or later (AGPL-3.0-or-later). If you modify this software and
make the modified version available to users over a network, the AGPL requires
you to offer those users the corresponding source code.
Portions derived from the original MIT-licensed pi-intercom project retain
their original notices. See THIRD_PARTY_NOTICES.md and
licenses/MIT-pi-intercom.txt. Versions already
published under MIT remain available under their original terms. See
LICENSE_TRANSITION.md for the exact commit and tag boundary.