Skip to main content
Atomic sessions can talk to each other. Press ALT+M to message another session, or ask the agent to coordinate with a peer.

Intercom

Atomic bundles @bastani/intercom, a first-party extension for direct 1:1 messaging between Atomic 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. Connections are lazy and tool-driven: the extension registers its commands and tools at startup, but a session does not connect until you or the model actually invoke Intercom. No separate install is needed. Key capabilities:
  • Session messaging - send, ask (blocking, 10-minute timeout), reply, pending, list, groups, and status via the intercom tool
  • Runtime groups - Add or remove named memberships without restarting; joined sessions keep their broker IDs and later subagents inherit the most recently joined membership
  • Session and group discovery - List connected sessions or every available group, including session counts and membership markers
  • Keyboard overlay - ALT+M or /intercom opens a session picker and compose overlay
  • Attachments - Share file, snippet, and context payloads between sessions
  • Subagent escalation - Delegated children get a contact_supervisor tool for decisions, structured interviews, and progress updates
  • Run notifications - Workflows and subagents deliver run results and control notices to a parent session over Intercom
  • Bundled skill - /skill:intercom provides planner-worker, group, and escalation-handling patterns
Example use cases:
  • Planner–worker splits across two terminals
  • Research → implementation context handoffs
  • Supervisor decisions and structured interviews for delegated subagents
  • Pair debugging between sessions

Table of Contents

Quick Start

From the Keyboard

Press ALT+M or run /intercom to open the session list overlay:
  1. Select a session — Use arrow keys to pick a target session
  2. Compose message — Write your message in the compose overlay
  3. Send — Enter Send · Escape Cancel
Sent messages are recorded in session history and confirmed with a notification.

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:

Receiving Messages

When a message arrives, it appears inline in your chat with the sender’s info and a reply hint:
The reply hint (enabled by default) points to intercom({ action: "reply", ... }), so recipients never 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 Atomic session history. Atomic treats ordinary intercom as a mandatory runtime tool in main chat and every workflow model stage. Tool allowlists, exclusions, noTools, optional-extension restrictions, and reloads cannot unload or deactivate it. Restrictions on every other tool are unchanged, and contact_supervisor remains subagent-only. Tool registration is lightweight; broker connection and heavy initialization remain lazy until an Intercom surface is used.

How Connection Works

Intercom connections are normally tool-driven. Ordinary sessions and delegated children load and register the lightweight wrapper at startup, while broker connection and heavy initialization wait until an Intercom tool, /intercom, or the ALT+M overlay is invoked. One exception is supervisor authorization: launching an Intercom-enabled subagent connects the parent runtime long enough to request a broker capability for that child; the child’s own connection remains lazy until it invokes contact_supervisor. The parent restores issued capabilities across reconnects, and the child uses the broker-confirmed current supervisor ID. Concurrent callers share one import and connection attempt, and broker state is leased to the active session generation and cleaned up on shutdown or replacement. A session becomes intercom-connected when all of these are true:
  • the mandatory bundled Intercom extension is loaded in that Atomic model session
  • the model or user has invoked an Intercom surface in that session, or the parent runtime is authorizing an Intercom-enabled child supervisor relationship
  • the local broker is running or can be auto-started
The session list only shows intercom-connected sessions, not every open Atomic process on the machine. Name sessions with /name so they can target each other (for example /name planner and /name worker). If a session is unnamed, Intercom exposes a runtime-only fallback alias like subagent-chat-1a2b3c4d-1111-4222-8333-123456789abc so other sessions can still target it. That alias is not persisted as the session title, so resume pickers keep showing the transcript snippet instead of a generic name.

The intercom Tool

Actions

To give two plain chat sessions a private shared membership, have both call:
Joining is additive: existing memberships remain active, and the broker updates presence without changing the session ID. Use intercom({ action: "groups" }) to discover all available names and membership markers. intercom({ action: "leave", group: "api-review" }) removes only that membership; intercom({ action: "leave" }) resets to the home group resolved at startup. Rejected or unacknowledged changes leave client and inheritance state unchanged. Ordinary delivery requires a shared membership, while contact_supervisor retains its capability-based cross-group path. Sent and received messages are recorded in session history as intercom_sent / intercom_received entries.

Targeting Sessions and Pending Workflow Stages

Live-session lookup accepts only an exact full Intercom session ID or an exact case-insensitive session name. Workflow stages use the canonical workflow:<rootRunId>/<segment>[/<segment>...] path printed by intercom list and workflow status surfaces; an exact target works while the row is PENDING and after it becomes RUNNING. Each segment may be a stage name, run id, or glob: * matches one segment and may be embedded, while ** matches any depth. Status surfaces label pending stages whose pre-start delivery capability is unavailable without presenting a usable target and never advertise a retained pending stage after its run terminates. The sessionId shown by workflow status belongs to the workflow SDK and is not an Intercom target. Before steering a stage from the main chat, enter the workflow invocation context by joining workflow:<rootRunId> with intercom({ action: "join", group: "workflow:<rootRunId>" }); workflow-owned invocation sessions already start there. A member of that invocation group can list, send to, and live-ask exact stages in any invocation-owned subgroup (workflow:<rootRunId>/<name>), including intentionally isolated reviewer batches. This control is directional: a session registered as a subgroup stage cannot gain parent control by joining the invocation group, subgroup members cannot discover or reach sibling subgroups, and another workflow invocation remains refused. PENDING accepts queued send only; RUNNING accepts immediate send and correlated ask/reply.

Deferred delivery to pending stages

Send material updates through Intercom to every affected workflow stage, including stages that have not started. Inside workflow:<rootRunId>, intercom list shows live sessions, materialized PENDING/RUNNING stages, and possible future literal, glob, and nested-child targets with queued counts. A deferred send returns queued, including its FIFO position, rather than claiming delivery; live matches receive the ordinary inbound message immediately. Name and pattern paths remain sticky for every future matching stage until the root terminates. When shared scope or acceptance criteria change, broadcast one authoritative update to workflow:<rootRunId>/** (or a narrower path pattern) rather than enumerating stages: ** reaches every live stage now and every future descendant. A syntactically valid path outside the persisted known set still queues and returns notInKnownSet; if it never delivers, root-terminal settlement sends the correlated undeliverable notification. A sticky entry delivered at least once is not reported undeliverable. The workflows extension persists up to 50 queued messages per target with workflow state. Messages survive resume/replay and broker restart, and logical message IDs prevent redelivery to the same materialized stage across stage-attempt restarts. When a matching stage session initializes, it receives the FIFO entries through the ordinary Intercom inbound path before its first model turn, under the heading Messages received before you started, with sender identity and Sent: timestamps visible separately from the task prompt. Only a workflow invocation member with eligible invocation-control authority can queue to its invocation-owned stages; this includes a main-chat session that explicitly joined workflow:<rootRunId>. Subgroup peers and another root run remain refused even if they add that membership. An explicit stage group: "default" is a shared-group escape, is not workflow-owned, and does not receive pending invocation delivery. An ineligible attempt is refused with Target workflow run is in a different intercom group. The 51st queued message is refused with Pending stage message queue is full (limit 50) rather than evicting an earlier entry. If the destination stage is skipped, the run terminates, or the stage becomes terminal before its session initializes, Atomic marks the queued message undeliverable and sends the correlated failure notification when acknowledgment was requested. Use ask only on a live, reply-capable exact target. Pending, future, and pattern asks return pending_stage_ask_unsupported; use ordinary send, because a stage may start much later or never start.

Groups

Every session belongs to a non-empty set of intercom groups. Sessions with no group configured retain exactly the legacy behavior: they belong only to the implicit "default" group and can see and message each other. A session can ordinarily discover, resolve, and message another session when their membership sets intersect; exact-ID sends with no shared membership are rejected by the broker.
  • list still lists sessions. Without a filter it returns the union of sessions visible through any of your memberships; with group, it gives a read-only view of that one group.
  • groups lists every currently available group, its connected-session count, and whether this session is a member.
  • join adds one membership. leave removes the named membership, while bare leave resets the complete set to the startup home group.
  • status reports the complete membership set. session_joined/session_left/presence_update events are delivered whenever a membership change affects visibility.
A session’s home group is resolved with this precedence: explicit stage/task/subagent group > runtime-owned workflow invocation group or inherited launching-session group > env ATOMIC_INTERCOM_GROUP (legacy PI_INTERCOM_GROUP) > Intercom config.json "group" > "default". Workflow stage named groups and group: true are namespaced under workflow:<rootRunId>/..., preventing cross-run collisions while preserving sibling isolation. group: "default" remains the explicit non-owned escape. The invocation group has asymmetric exact-target control over its owned subgroups; ownership does not grant reverse or lateral access. The broker, not the client, marks validated supervisor traffic. Ordinary send frames remain membership-isolated even if a raw client forges a supervisor marker, and replies cross back only through an exact broker-recorded replyTo match. Parent-held authorization state is restored after reconnects. Before an Intercom-enabled foreground child first runs, the parent wrapper may lazy-load and connect the broker provider to mint that exact child’s capability; queued children request no capability. The child still connects only when it uses an Intercom delivery path, and claimed decisions or interviews terminally hand off before child send or waiter admission. A claimed provider failure aborts launch, while runtimes with no provider omit supervisor metadata and do not expose a broken channel.

send vs ask vs reply

send is fire-and-forget — the tool returns immediately after delivery. By default it sends immediately, including 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 continue without an extra approval step. ask normally sends the message and blocks until the recipient responds (10-minute timeout). If the recipient disconnects after delivery, only the exact ask to that peer fails promptly; the timeout remains the backstop for a connected but unresponsive recipient. Up to maxPendingAsks waits (default: 6) may run concurrently, including same-target and mixed-target fan-out. Exact sender/message correlation keeps out-of-order replies and selective disconnects from cross-settling another call. A foreground child asking its resolved launching parent is the exception: Atomic ends the child before send or waiter admission and returns a dynamic [TASK_CONTEXT] handoff through the parent subagent call. Multiple children may hand off independently; each request is keyed by child/run identity and each request has a first-claim-wins owner. contact_supervisor keeps a narrower policy: one blocking decision/interview wait per child may coexist with ordinary peer asks, but a second concurrent supervisor wait receives Already waiting for a supervisor reply. Claimed foreground handoffs allocate no waiter. Mutual peer asks are supported, although both sessions must process inbound work to reply; the per-waiter timeout remains the backstop. reply is receiver-side sugar for replying to an inbound ask. In the turn triggered by an incoming intercom message, intercom({ action: "reply", message: "..." }) targets that exact sender and message automatically. If you reply later, it falls back to the single unresolved inbound ask; with multiple pending asks, use pending and pass to, or pass the listed message ID as replyTo to disambiguate multiple asks from the same sender. Under the hood this is still a normal send with the exact replyTo value.

Attachments

send, ask, and reply accept an attachments array of { type, name, content, language? } objects where type is "file", "snippet", or "context". Attachment content is included in the recipient’s agent-visible message body. When a parent-targeted foreground ask is terminally handed off at the source, the same ordered attachment array is retained and rendered with the question for the launching parent; duplicate names and content are not rewritten. Attachments are supported in the protocol but not in the ALT+M compose overlay.

Coordination Patterns

The most natural use of Intercom is splitting a task between two sessions — one holds the big picture, the other does the hands-on work. Open two terminals, start Atomic in each, and name them so they can find each other:
Verify they see each other with intercom({ action: "list" }), then coordinate:
The bundled intercom skill (/skill:intercom) has copy-paste ready patterns for planner-worker delegation, status checks, natural replies, broadcasting to multiple workers, attachments, and handling subagent escalations on the orchestrator side. Recommended: Add this snippet to your project’s AGENTS.md to help agents understand when to coordinate across sessions:

Subagent Escalation: contact_supervisor

When Atomic’s subagent runtime admits a delegated child, 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 is registered from the typed admission record. The record binds the supervisor target, canonical child identity, child index, session name, and any broker-issued capability to that in-process child session; none of those values are inherited from environment variables. If the parent did not grant supervisor coordination, the session receives only the regular intercom tool. A parent-targeted blocking ask makes the current child terminal for continuation. The handoff identifies the previous agent and run, but follow-up uses a fresh child and new run identity. Ordinary Intercom detach remains separate for sends, progress updates, and non-parent asks.

The Three Reasons

Do not use contact_supervisor for routine completion handoffs—return the final subagent result normally. Blocking reasons are intercepted before broker connection or reply-waiter admission when the exact foreground child claims them. If no live owner claims a request, the existing Intercom send/wait fallback remains available.

What the Supervisor Sees

For a claimed foreground parent ask, the supervisor receives terminal run metadata, the original question, ordered attachments, the previous agent identity, and an explicit fresh-start call:
The generated task context includes the original delegated task, what the previous child was working on, the question, and the supervisor answer placeholder. Parallel asks do not retain active sibling sets; any follow-up is an explicit fresh launch.

Structured Interview Replies

interview_request questions use the shape { id, type, question, options?, context? } where type is single, multi, text, image, or info (info questions are context-only and need no response):
The handoff includes the structured questions without reordering or rewriting them. The supervisor can include a plain or fenced JSON answer in the fresh child task; this stable shape keeps answers tied to question IDs:
Atomic preserves the supplied answer in the fresh task context. The parent-ask handoff does not create an Intercom reply or a structuredReply tool-result field. An unclaimed fallback request keeps the existing Intercom structured-reply parsing behavior.

Workflow and Subagent Notifications

Intercom is also the delivery channel for workflow run results and subagent control notices from workflows and subagents.

Workflow Delivery Modes

Programmatic workflow() calls accept an intercom option that controls how asynchronous direct-run results and control notices reach a parent session:
When neither enabled nor delivery is set, direct parallel runs default to control-and-result when Intercom is available; otherwise delivery is off. Treat Intercom payloads from direct runs as user-visible workflow output. While a workflow stage generation is open, incoming Intercom messages are admitted through the stage session’s native steering/follow-up queue. Parent-targeted blocking asks from that stage’s own foreground child bypass destination delivery: the child ends at the source and returns a fresh-child handoff through the stage’s subagent call. Other messages keep the destination-side reservation and exact-child probe/commit detach handshake, so terminal stage close cannot overtake an admitted delivery. A destination-side admission failure returns a correlated actionable error to a blocking non-parent asker instead of waiting for the 10-minute reply timeout.

Subagent Control Notices

The subagent tool’s control options select which control events notify the parent and over which channels:
  • notifyOn — defaults to ["active_long_running", "needs_attention"]
  • notifyChannels — defaults to ["event", "intercom"] (all that are available)
Detached subagent result delivery over Intercom is confirmation-based and preserves a successful delivery phase across watcher replacement. Each delegated child gets a deterministic Intercom target derived from its run/agent/index identity, and run results report those targets (“Run intercom target” / “Previous intercom target”; targets may be inactive after completion). intercom({ action: "status" }) reports connection state and every membership for the current session. If live peer coordination is needed, invoke intercom({ action: "status" }) in the parent before launching; the child connects on its first ordinary Intercom call. A claimed contact_supervisor decision or interview can yield before child broker connection because typed admission already identifies the launching parent. Fresh child sessions always receive the mandatory bundled Intercom wrapper, including when an explicit extensions allowlist is empty or omits it.

Delivery Ordering

Blocking contact_supervisor decisions and interviews, plus intercom.ask calls whose resolved target is the launching parent, end at the source before Intercom send or waiter admission. The parent receives the verbatim question, ordered attachments, child identity, and fresh-start handoff. In parallel, the claim interrupts active siblings and prevents queued work from starting without retaining the sibling set. Progress updates, sends, and asks to other peers retain the probe/commit detach path. For delegated children, queued messages and terminal lifecycle notices remain ordered per child. Exact terminal-identity deduplication prevents double admission, failed dispatches remain retryable, and correlated ask replies bypass unrelated queued sends. See Subagents for the full coordination contract.

Configuration

Create ~/.atomic/agent/intercom/config.json. The legacy ~/.pi/agent/intercom/config.json fallback is read when the Atomic config is absent:
The default npx --no-install tsx pair is a compatibility sentinel: Intercom recognizes it and starts the broker through the current Atomic runtime (process.execPath). It never resolves or executes tsx — Node-based installs run the broker with Atomic’s bundled jiti loader, which is dependency-free pure JavaScript; Bun source-checkout runs use the current Bun executable directly; standalone Atomic binaries re-enter the split launcher through a narrow internal broker handoff. Default startup therefore does not rely on npx, tsx, or bun being on PATH. Explicit custom broker commands still work — for example, to intentionally use Bun from PATH:
Config validation is strict: every field is checked, and if the file is not valid JSON or any field has an invalid value, the whole config is rejected — an error is logged and all defaults are used. 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 completion. A configured status is appended as context instead of replacing the lifecycle status.

Keyboard Shortcuts

How It Works

The broker is a standalone process that manages session registration and message routing. It auto-spawns when the first session that invokes Intercom needs it and exits 5 seconds after it last has no registered sessions, including brokers that never received a connection and sockets that close before register; clients reconnect automatically if the broker restarts. A reconnect that fails schedules the next attempt on a bounded backoff (1s, 2s, 5s, 10s, then 30s) and keeps retrying until the session connects or shuts down, so recovery never waits for an explicit Intercom call. A failed explicit intercom or overlay connection surfaces its error to the caller and still leaves that background retry in place. A reconnect that fails after the broker already accepted it closes that connection first, so a session never appears twice in intercom list. A spawn lock keyed by PID and timestamp prevents duplicate brokers when multiple sessions start at once. A recoverable disconnect is only reported where someone is waiting on it. Work Intercom starts on its own — eager workflow-stage warm-up during session_start, the background subagent and pending-stage event relays, and the advisory supervisor-authorization request made before a child launches — does not surface such a disconnect as a stage error; the stage keeps running, and a launch proceeds with supervisor metadata omitted rather than aborting. Recovery still has an owner in every case. Once the heavy module exists, the bounded reconnect backoff above owns it. Warm-up is the one point where no heavy module exists yet to run that backoff, so the wrapper itself retries the warm-up on the same bounded schedule, and only reports if those attempts run out — which matters because a stage holding queued messages waits for that first successful delivery. Explicit intercom calls, /intercom, and the ALT+M overlay still fail visibly. Protocol, authentication, configuration, non-recoverable initialization, and terminal relay failures are reported on every path, as is an exhausted warm-up retry. Classification is by the error type raised inside the broker client, not by message text, so an identically worded failure from anywhere else stays actionable. A drop that first surfaces as a socket error on an already-registered connection — ECONNRESET, EPIPE, and the rest — is a recoverable disconnect and enters the same bounded recovery, with the original transport error kept as the cause so the code is still there to read. A framing or protocol error keeps its own Intercom protocol error: … diagnosis even when a socket error follows it, and a failure before registration completes is never reclassified. On the broker side, a session is retired as soon as its socket stops being able to accept a frame, rather than only when the connection finally closes. A peer that half-closes, or one whose connection the broker itself ended after refusing a registration, can hold its read side open indefinitely; leaving it in the routing table meant every later broadcast wrote into a socket whose writable side was gone, which destroys that socket and floods broker.log. Every broker write now checks writability as part of the write itself. Delivery-producing sends also wait for the socket write callback, so an immediate asynchronous reset cannot be recorded as a successful delivery. A write that fails is answered Session not found, the message id stays retryable rather than being recorded as delivered, and no reply authorization is opened for a message that was not sent. Transport is local IPC only — a Unix domain socket on macOS/Linux or a named pipe on Windows — using length-prefixed JSON (4-byte length + payload) with request correlation for session listing, explicit delivery failures, and validation of malformed or out-of-order messages. ask stays client-side: the broker routes plain messages, and the client waits for the matching reply before returning it as the tool result. Runtime files live under the active agent directory — ~/.atomic/agent/intercom/ by default, or below ATOMIC_CODING_AGENT_DIR when set (the legacy PI_CODING_AGENT_DIR alias is honored when the Atomic variable is unset):
  • broker.sock — Unix domain socket (macOS/Linux; Windows uses a named pipe instead)
  • broker-launch.vbs — Windows helper script to launch the broker without a console window
  • broker.pid — Broker process ID
  • broker.spawn.lock — Short-lived lock used to avoid duplicate auto-spawns
  • broker.log — Broker stderr, truncated on every spawn and capped at 8 KiB by the broker itself
  • config.json — User configuration
The broker runs as a detached subprocess, so it does not share the host session’s module graph: every module it loads resolves from Node built-ins and Intercom’s own files only. Standalone Atomic binaries run it through the internal broker handoff of the same executable, with no external runtime package to resolve. If the broker fails to start, its stderr is not lost. The parent hands the child an already-open descriptor on broker.log (a file, not a pipe, because the broker outlives the session that spawned it) and truncates the file on every spawn. Both the “exited before startup” error and the readiness-timeout error quote the log path and a bounded tail of that output, so cat ~/.atomic/agent/intercom/broker.log shows the same text after the fact. On Windows the hidden launcher appends the broker’s stderr to the same file. The file cannot grow without limit. The parent exits while the broker keeps running, so the cap is applied inside the broker, by the entrypoint’s very first import — ESM evaluates a module’s static dependencies before the importer’s own body, so anything installed later would leave those dependencies free to write first. Three routes reach the log and each is capped: process.stderr.write, console.error / console.warn, and the default fatal printing for an uncaught exception or an unhandled rejection. Patching the stream alone would not be enough — Bun’s console writes to the file descriptor directly, and neither runtime routes a fatal error through the stream. Anything past 8 KiB is discarded rather than written, on the direct launch and the Windows redirect alike. What the cap cannot cover, stated plainly: diagnostics a runtime or loader emits before that first import evaluates, native code writing straight to file descriptor 2, a child process of the broker, and hard termination. The broker’s own module graph contains no child-process or native-addon edge. Async extension work (startup, inbound flushes, reconnects, overlays, and relays) no-ops if the session shuts down or reloads before it settles.

Intercom vs Shared-Room Messengers

Use a shared-room messenger for multi-agent swarms working on one shared task. Use Intercom when you want to manually coordinate your own sessions or have one agent reach out to another specific session.

Limitations

  • Same machine only — Uses local sockets/pipes, no network support
  • No dedicated intercom log — Messages are kept in session history; there is no separate intercom transcript or inbox
  • No attachments UIfile, snippet, and context attachments are supported in the protocol, but not in the compose overlay
  • Only connected sessions appear — The list shows sessions that have connected to the broker, not every open Atomic process
  • Broker lifecycle — The broker auto-spawns on first use and exits when idle; sessions reconnect automatically if it restarts
  • Subagents for delegated child runs, foreground coordination, and result delivery.
  • Workflows for multi-stage automation and run notifications.
  • Skills for reusable instructions like /skill:intercom.
  • Usage for environment variables and the bundled-extension overview.