@diegopetrucci/pi-intercom
v0.8.0
Published
Local session-to-session intercom extension for Pi coding agent and tlh workflows.
Readme
Pi Intercom
Direct 1:1 messaging between pi sessions on the same machine. Send context, findings, or requests from one session to another — whether you're driving the conversation or letting agents coordinate.
TLH fork note: this fork exists to serve The Last Harness (tlh), where tlh automation bundles and pins this package version on purpose. It is maintained as a TLH compatibility fork, not as a general standalone distribution target outside that tlh use.
User flow: press Alt+M or run /intercom to pick a session and send a messageWhy
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. Modern pi-subagents provides native child-to-supervisor tooling. For compatibility, pi-intercom's full surface still registers its legacy contact_supervisor implementation when child bridge metadata is present; bridge omits that duplicate and relies on native pi-subagents supervision. Normal sessions never receive the child-only tool.
In One Minute
Each pi session that has pi-intercom loaded, enabled, and a non-off surface connects to a tiny local broker over a local IPC transport. In the default full surface, the extension gives you both a tool (intercom) and a small overlay UI (/intercom or Alt+M). In bridge, it keeps the broker/runtime path needed for subagent rich-result and control relays but omits the local intercom tool and pi-intercom's compatibility supervisor tool. Incoming messages are rendered inline inside the recipient session, can trigger a turn immediately, and are also stored in Pi session history as extension entries.
Install
pi install npm:@diegopetrucci/pi-intercomThen restart Pi. The extension auto-connects to the broker on startup and registers the bundled pi-intercom skill for common coordination patterns.
If you're using The Last Harness (tlh), keep using the version bundled or explicitly pinned by tlh automation rather than treating this fork as a generally floating standalone install target.
For The Last Harness (tlh), pin the exact fork version instead of relying on an unpinned install:
pi install npm:@diegopetrucci/[email protected]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 pi sessions on related codebases. 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$PI_CODING_AGENT_DIR/intercom/config.json(defaults to~/.pi/agent/intercom/config.json)- the effective surface is not
off - the session has started or reloaded after the extension was installed
- the local broker is running or can be auto-started
For tlh migrations, PI_INTERCOM_SURFACE is the intended temporary runtime override because it changes the exposed surface without rewriting user-owned config.json.
The session list only shows intercom-connected sessions, not every open Pi process on the machine.
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
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
- Compose message — Write your message in the compose overlay
- Send — Press Enter to send, Escape to cancel
From the Agent
The agent can list sessions and send messages using the intercom tool. Tool calls and results render as compact transcript rows so send/ask/reply flows are easy to scan. For common patterns like planner-worker delegation, the bundled pi-intercom skill provides copy-paste ready examples:
// List active sessions
intercom({ action: "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({ action: "send", to: "research", message: "Check if UserService.validate() handles null" })
// → Message sent to research
// Check connection status
intercom({ action: "status" })
// → Connected: Yes, Session ID: abc123, Active sessions: 3
// Send with attachments (code snippets, files, or context)
intercom({
action: "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({ action: "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({ action: "reply", ... }), so recipients do not need raw sender or replyTo IDs. Idle recipients get a new turn immediately; busy interactive recipients receive the message once they go idle. Attachment content is included in the agent-visible body, and messages are rendered inline and stored in Pi session history.
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 workerVerify they see each other from either session:
intercom({ action: "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 sends the message and blocks until the planner replies, so the worker gets the answer as a tool result and continues in the same turn.
Planner sends a task:
intercom({
action: "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({
action: "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({
action: "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({
action: "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() call to respond:
**From planner** (~/projects/api)
To reply, use the intercom tool: intercom({ action: "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 doesn't need to reconstruct raw to and replyTo IDs — the hint is right there. 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({ action: "reply" }) falls back to the single unresolved inbound ask, and intercom({ action: "pending" }) shows who is still waiting.
send vs ask
send is fire-and-forget — the tool returns immediately after delivery. By default, it sends immediately even in interactive sessions. If you want an approval dialog before non-reply sends, set confirmSend: true in config. Replies that include replyTo still skip confirmation so reply-hint flows can continue without an extra approval step.
ask sends the message and blocks until the recipient responds (2-minute timeout by default). The reply comes back as the tool result, so the agent continues in the same turn with full context. No confirmation dialog — if you're asking and waiting, the intent is clear.
reply is receiver-side sugar for replying to an inbound ask. In the turn triggered by an incoming intercom ask, intercom({ action: "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 intercom({ action: "pending" }) to inspect them and then call reply with to to disambiguate.
The planner typically uses send. If you prefer manual approval for outgoing non-reply messages, turn on confirmSend: true. The worker uses ask for everything (no confirmation needed, gets answers inline), so it can operate autonomously either way.
Workflow: Subagent-to-Supervisor Escalation
This workflow requires pi-subagents. Modern pi-subagents owns and provides native child-to-supervisor supervision. In full mode, pi-intercom also retains its legacy/compatibility contact_supervisor implementation when child bridge metadata is present; this sits alongside the regular intercom tool. In bridge mode, pi-intercom deliberately omits that duplicate and relies on the native pi-subagents supervisor tool. Normal sessions never receive a child-only contact_supervisor tool.
The temporary bridge surface remains responsible only for rich subagent result/control delivery compatibility during tlh migration work. It does not replace or provide native supervisor tooling.
When pi-intercom's Compatibility Tool Appears
In full mode, pi-intercom's compatibility contact_supervisor 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, pi-intercom does not register its compatibility contact_supervisor; in full mode its regular intercom tool remains available. This condition does not describe registration of modern pi-subagents' native supervisor tool.
Foreground vs Async Child Sessions
Blocking supervisor contact requires a live reply path. pi-subagents async/background launches provide that path, so need_decision and interview_request can wait for your reply. Foreground launches mark the reply path unavailable instead: blocking asks fail fast before any intercom message is sent, while progress_update and other non-blocking sends still work.
If you expect a child to need decisions while it is running, launch it async/background. If a foreground child still gets blocked, have it return the blocker in its final result instead of trying to wait through contact_supervisor or intercom({ action: "ask" }).
Three Reasons
| Reason | Behavior | Use When |
|--------|----------|----------|
| need_decision | Sends an ask and blocks until the supervisor replies (2-minute timeout by default) | The subagent is blocked, uncertain, needs approval, or faces a product/API/scope decision |
| interview_request | Sends structured questions and blocks until the supervisor replies (2-minute timeout by default) | 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 plannerWhat 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/reply flows. The supervisor can reply with intercom({ action: "reply", message: "..." }) and the subagent receives the answer as the tool result. If the child completes or disconnects before you reply, the pending ask expires and pending/reply tell you to use the completed child session or artifact path for follow-up instead of sending into a dead session.
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
intercom
| Parameter | Type | Description |
|-----------|------|-------------|
| action | string | "list", "send", "ask", "reply", "pending", or "status" |
| to | string | Target session name or ID (for send/ask, or to disambiguate reply) |
| message | string | Message text (for send/ask/reply) |
| attachments | array | Optional file, snippet, or context attachments |
| replyTo | string | Optional message ID for threading or replying to an ask |
contact_supervisor
Modern pi-subagents provides the native supervisor tool. Pi-intercom registers the compatibility implementation documented below only in full-surface sessions where pi-subagents supplied the required child bridge metadata; bridge omits it to avoid duplicating the native tool. It 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 (2-minute timeout by default) when the child has a live reply path. Async/background subagents get that live path; foreground children fail fast here and should return blockers in their final result instead. 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 (same 2-minute default timeout) when the child has a live reply path. Foreground children fail fast here just like need_decision, so they should report blockers in their final result instead of waiting. 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": ... }] }. Parsed JSON replies are returned in details.structuredReply.
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.
intercom actions
list — Returns the current session plus other active intercom-connected sessions with name, short ID, working directory, model, and live status. Status is derived automatically from Pi lifecycle events: idle, thinking, or tool:<name>.
send — Sends a message to the specified session. By default it sends immediately, including in interactive sessions. Set confirmSend: true in config if you want a confirmation dialog for non-reply sends. Replies that include replyTo skip confirmation. Returns delivery confirmation.
ask — Sends a message and waits for the recipient to reply (2-minute timeout by default). The reply is returned as the tool result. No confirmation dialog. Only one pending ask is allowed per session at a time. Use this when the agent needs the answer to continue working.
reply — Replies to the current intercom-triggered message if there is one. Otherwise it falls back to the single unresolved inbound ask. If multiple asks are pending, pass to or inspect them with pending first. Under the hood this is still a normal send with the exact replyTo value.
pending — Lists unresolved inbound asks with sender, message ID, elapsed time, and a short preview. Useful when replying after the original triggered turn.
status — Shows connection status, session ID, and total count of active sessions (including the current session).
Keyboard Shortcuts
| Key | Action | |-----|--------| | Alt+M | Open session list overlay | | ↑/↓ | Navigate session list | | Enter | Select session / Send message | | Escape | Cancel / Close overlay |
Config
Create $PI_CODING_AGENT_DIR/intercom/config.json (or ~/.pi/agent/intercom/config.json when PI_CODING_AGENT_DIR is unset):
{
"brokerCommand": "npx",
"brokerArgs": ["--no-install", "tsx"],
"confirmSend": false,
"enabled": true,
"surface": "full",
"replyHint": true,
"showIncomingMessages": true,
"status": "researching"
}Surface modes
surface controls registration of intercom-facing tools, UI, and runtime. enabled remains a separate legacy runtime switch: false suppresses broker/session connection and broker-required operations, but it does not unregister public resources and local subagent event relay may still operate. Use surface: "off" for the hard stop.
| Surface | Default | What it does |
|---------|---------|--------------|
| full | yes | Default behavior: registers the intercom tool, /intercom, Alt+M, normal reply hints, and pi-intercom's compatibility contact_supervisor when pi-subagents provides child metadata |
| bridge | no | Keeps broker/runtime compatibility for temporary tlh migration bridging, including rich subagent result delivery and control relay behavior, but omits the local intercom tool, /intercom, Alt+M, reply-hint tool instructions, and pi-intercom's duplicate compatibility contact_supervisor |
| off | no | Installs no intercom runtime or public surface |
bridge is intentionally narrower than full: it is a temporary compatibility surface until native pi-subagents has rich-result/control delivery parity, not a replacement for native supervisor tooling (which modern pi-subagents already provides).
PI_INTERCOM_SURFACE precedence
The effective surface is resolved in this order:
PI_INTERCOM_SURFACE, if setsurfacein$PI_CODING_AGENT_DIR/intercom/config.json- the default
full
Accepted values are full, bridge, and off.
If PI_INTERCOM_SURFACE is set to an invalid value, pi-intercom warns and fails safe to full. This override still wins over config.json, so an invalid environment value will not silently leave the session in bridge or off.
If config.json#surface is invalid, pi-intercom warns and falls back safely to full unless a valid PI_INTERCOM_SURFACE override is present.
To undo a temporary override, unset PI_INTERCOM_SURFACE and reload/restart the session. For example:
unset PI_INTERCOM_SURFACETo undo a checked-in or local config override instead, remove the surface key (or set it back to "full") in $PI_CODING_AGENT_DIR/intercom/config.json, then reload/restart the session.
Other settings
| Setting | Default | Description |
|---------|---------|-------------|
| brokerCommand | "npx" | Command used to start the local broker process |
| brokerArgs | ["--no-install", "tsx"] | Arguments passed to brokerCommand before the broker script path |
| confirmSend | false | Show a confirmation dialog before non-reply sends from an interactive session with UI |
| enabled | true | Legacy runtime switch. When false, suppresses broker/session connection and broker-required operations; it does not unregister public resources, and local subagent event relay may still operate. Use surface: "off" for a hard stop |
| surface | "full" | Choose full, bridge, or off as described above |
| replyHint | true | Include reply instruction in incoming messages when the full tool surface is active |
| showIncomingMessages | true | Render inbound message boxes in the TUI; when false, messages are still delivered to the model |
| status | — | Optional custom status suffix shown after the automatic lifecycle status, for example thinking · researching |
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.
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| B3The 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 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, and validation for malformed or out-of-order messages.
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_CODING_AGENT_DIR/intercom/ (default ~/.pi/agent/intercom/):
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 IDconfig.json— User configuration
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.
Auto-spawn with file lock. The broker starts on first connection and exits after 5 seconds idle. There is no daemon to manage. A spawn lock file, keyed by PID and timestamp, prevents duplicate brokers when multiple sessions start at once.
ask stays client-side. The broker still routes plain messages; it does not have a special request/response mode for ask. The client waits for a matching reply before it triggers a new turn, then returns that reply as the tool result. Reply hints make that flow practical by showing the recipient the exact send call to use. Separately, list / sessions now carry a requestId so a delayed session-list reply cannot be mistaken for a newer one.
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 | In 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_CODING_AGENT_DIR/extensions/pi-intercom/ # defaults to ~/.pi/agent/extensions/pi-intercom/
├── package.json
├── index.ts # Extension entry point
├── types.ts # SessionInfo, Message, protocol types
├── config.ts # Config loading
├── 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
└── skills/
└── pi-intercom/
└── SKILL.md # Bundled skill for common patternsLimitations
- Same machine only — Uses local sockets/pipes, no network support
- No dedicated intercom log — Messages are kept in Pi session history, but there is no separate intercom transcript or inbox
- 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
