Gives each pi session a unique persistent identity so AI agents can collaborate across sessions via intercom.
- Pi extension (
agent-identity/index.ts): Name generation, system prompt identity rules, daemon client, git commit co-author hook - Detached daemon (
agent-identity/daemon.ts): Singleton Unix socket server, offline (ghost) agent tracking, disconnected session revival
pi install git:github.com/elecnix/pi-agent-identity- Each pi session gets a random name like
swift-koala-42 - Sessions register with a detached singleton daemon via Unix socket
- The daemon keeps tracking disconnected agents so they remain reachable — but hidden from the intercom roster list;
agent_searchfinds them by name or ghost id - Disconnected sessions are revived: the daemon spawns
pi --session <file> -p "message"when an intercom message arrives for an offline agent - Agent lookup: The daemon exposes a
lookup_agentmessage so pi core can resolve agent names to session files, enabling--session <name>(requires pi >= next release after PR earendil-works/pi#5987) - Filesystem fallback: If the daemon has no record of an agent (e.g. after a reboot wiped its
/tmpregistry), lookup falls back to scanning~/.pi/agent/sessions/**/*.jsonlfor the session whose embeddedagent-identity-namematches, and resumes the most recent one. The daemon registry is a cache; the session files are the durable source of truth. Override the scan root withPI_SESSIONS_DIR. - Addressing by immutable identity: the agent identity (e.g.
keen-gar-77) is the addressable subject; the session name is mutable display metadata. On intercomsend/ask, a bare identity is resolved against pi-intercom's live roster (via its extension-bus channel, ≥ 0.12) and rewritten to the registered composite name (keen-gar-77: fix-auth-bug) when needed — so peers stay addressable by bare name aftersession_rename. Each session also publishes{ agentName }on the silentagent-identity/v1bus namespace. If delivery still fails, the daemon relay reports honestly what happened (live delivery, revival, deferred, unresolved) and never spawns a secondpiprocess against a live session file.
Every pi session runs two extensions that talk to two independent singletons:
flowchart LR
subgraph SenderPi["pi session (sender)"]
S1["intercom extension"]
S2["agent-identity extension"]
end
subgraph Broker["pi-intercom broker<br />(live-session message router)"]
B1["roster of connected sessions"]
end
subgraph Daemon["agent-identity daemon<br />(singleton, unix socket)"]
D1["registry: agent-name → session-file<br />pid, connected flag, cwd, repo"]
end
S1 -->|message frames| B1
S2 -->|register / lookup / queue_mention| D1
D1 -.->|revival: spawn pi with saved session| R1
R1["revived pi session (receiver)"]
- pi-intercom has no durable memory: it routes text only between currently connected sockets. Offline agents deliberately do not appear in its roster.
- agent-identity daemon is the control plane: it knows every agent that ever registered — online or not — and is the only component that can revive one.
When the target is connected, the daemon is not involved at all:
sequenceDiagram
autonumber
participant S as Sender (keen-gar-77)
participant RW as identity-addressing hook
participant IC as intercom extension
participant BR as broker
participant R as Receiver (amber-wolf-66)
S->>RW: intercom({ action: "send", to: "amber-wolf-66" })
RW->>RW: resolve bare identity against live roster<br />(rewrite to "amber-wolf-66: task-name")
RW->>IC: send(to=rewritten name)
IC->>BR: route to live socket
BR->>R: message
BR-->>IC: delivered
IC-->>S: details: { delivered: true }
If the target is offline, the broker cannot find it. The send fails, and the agent-identity extension catches that failure on the tool result and routes the message through the daemon instead:
sequenceDiagram
autonumber
participant S as Sender (keen-gar-77)
participant RW as identity-addressing hook
participant IC as intercom extension
participant BR as broker
participant ES as identity extension (tool-result hook)
participant D as agent-identity daemon
participant RV as revived pi (amber-wolf-66)
S->>RW: intercom({ action: "send", to: "amber-wolf-66" })
RW->>IC: send(to="amber-wolf-66")
IC->>BR: route
BR-->>IC: E_TARGET_NOT_FOUND<br />(not in live roster)
IC-->>S: details: { delivered: false, code }
Note over ES: hook on intercom tool_result
ES->>ES: extract target + message body
ES->>D: queue_mention { targetName, fromName, body }
D->>D: resolveDaemonTarget(name, registry)
alt target connected
D-->>ES: mention_queued { method: "live" }
else process alive, socket unreachable
D-->>ES: mention_queued { method: "deferred" }<br />(do NOT spawn a second writer)
else pid dead
D->>RV: spawn pi --session <file> -p <message>
D-->>ES: mention_queued { method: "revival" }
else no registry match
D-->>ES: mention_queued { method: "unresolved" }
end
ES-->>S: honest report (live / revival / deferred / unresolved)
flowchart TD
Q[queue_mention received] --> RES{resolveDaemonTarget}
RES -->|no match| UN[method: unresolved<br />nothing relayed]
RES -->|match| C{target.connected?}
C -->|yes| L[method: live<br />deliver over daemon socket]
C -->|no| A{process pid alive?}
A -->|yes| DF[method: deferred<br />do NOT spawn - two writers risk<br />on one session file]
A -->|no| SP[revive: spawn pi on saved session file]
SP --> V[method: revival]
Revival is deliberately conservative: the daemon never spawns a second pi
against a session file whose process is still alive, and it reports what it
actually did instead of pretending an offline peer got the message.
JSON messages of { type, ... } over the unix socket, one per line.
| Message | Request | Response |
|---|---|---|
register |
{ type: "register", agentName, sessionFile, pid, repo?, cwd? } |
{ type: "ack", agentName }, updates the in-memory registry and persists it to /tmp/agent-identity-daemon-registry.json |
unregister |
{ type: "unregister", agentName } |
— (used on explicit shutdown; sessions normally stay registered so they can be revived) |
lookup_agent |
{ type: "lookup_agent", agentName } |
{ type: "agent_found", name, sessionFile, connected, pid, active, repo } or { type: "agent_not_found", agentName } |
list_agents |
{ type: "list_agents" } |
{ type: "agent_list", agents: [...] } (used for collision-free name minting) |
queue_mention |
{ type: "queue_mention", targetName, fromName, body } |
{ type: "mention_queued", targetName, method } where method ∈ live | revival | deferred | unresolved |
ping |
{ type: "ping" } |
{ type: "pong" } (liveness) |
version_check |
{ type: "version_check", version } |
{ type: "version_ok", version } or { type: "version_mismatch", expected, received } |
The relay is best-effort by design. Messages can be lost when any of these hold:
- the daemon is down or the socket is stale — the tool-result hook bails on
!socket.writableand no relay attempt happens at all; - the target process is alive but socket-unreachable — the daemon replies
deferredand does not keep the message; - the broker's 24h mailbox for recently-disconnected sessions is evicted
(
MAX_MAILBOX_MESSAGES) or pruned afterDISCONNECTED_SESSION_RETENTION_MS; - the daemon does not answer within the 3s relay verdict window
(
RELAY_VERDICT_TIMEOUT_MS), leaving the sendunresolved; - the agent name never registered —
queue_mentionresolves to no entry and returnsunresolved.
The extension reports each outcome honestly (buildRelayReport), so the sender
never believes a message was delivered when it was not.
| Command | Description |
|---|---|
/whoami |
Show your agent identity |
/agent-status |
Daemon connection status |
/agent-reconnect |
Force reconnect to daemon |
/resume-agent |
Switch to another agent's session by name |
| Tool | Description |
|---|---|
session_rename |
Let the agent rename its own session to reflect the task. The agent name stays as the prefix (keen-gar-77: fix-auth-bug). The description instructs the LLM to call it immediately on the first user message that conveys intent. |
agent_search |
Search registered agents by (partial) name — exact matches rank first, then prefix matches — returning full registered intercom names and online/offline status. Resolves addresses from a short fragment without dumping hundreds of roster entries into the transcript. |
ask to an offline (revivable) agent cannot block — a revived session has no reply-waiter. Instead of failing hard with "Session not found", the relay revives the target with the questions framed for an answer-back, and reports honestly that the reply will arrive asynchronously.
Offline targeting (#7) and hidden ghosts
Disconnected-but-revivable agents are hidden from the intercom roster list (the daemon no longer registers ghost sessions with the broker). They stay addressable under a deterministic id (agent-<agent-name>): agent_search surfaces offline agents with this id, and a message sent by name or id that fails at the broker is routed through the daemon's queue_mention, which revives the real session. The queued message redelivers when the live session returns in the same project.
Fresh names are drawn collision-free: before adopting a minted name, the extension queries the daemon's registry (list_agents) and re-rolls (up to 10 draws) if the name still belongs to a disconnected-but-revivable agent. This prevents a same-name remint from splitting an identity's message routing (#34).
The extension exposes the agent identity to every LLM-callable bash subprocess as PI_AGENT_NAME. The name is set on the pi process at session start via process.env, so spawned shell children inherit it; pi resolves the bash env per execution, so every bash tool command sees the current agent name.
pi core's own session metadata vars (PI_SESSION_ID, PI_PROVIDER, PI_REASONING_LEVEL, etc.) are unaffected. To pin an identity before launch, just pass AGENT_IDENTITY_NAME (already used for testing and daemon revival):
AGENT_IDENTITY_NAME=swift-koala-42 pi -p "run a bash command"