Cross-session agent communication for GitHub Copilot CLI — lightweight IPC mesh using SQLite.
Each GitHub Copilot CLI session runs in complete isolation. If you have terminals open in different repos — say a frontend app and a backend API — those agents can't talk to each other. There's no built-in way for one session to ask another session a question, delegate work, or share results.
agent-mesh bridges that gap. It gives every Copilot CLI session the ability to discover peers and exchange messages across sessions, using nothing but a shared SQLite database on your machine.
┌─────────────────────────────────────────────────────────────────┐
│ Your Machine │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Terminal 1 │ │ Terminal 2 │ │ Terminal 3 │ │
│ │ my-frontend │ │ my-api │ │ infra │ │
│ │ (Copilot CLI)│ │ (Copilot CLI)│ │ (Copilot CLI)│ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ │ ┌──────────────┴──────────────┐ │ │
│ └────┤ agent-mesh.db (SQLite) ├────┘ │
│ │ WAL mode · lock-free │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ agent_sessions │ │ │
│ │ │ agent_messages │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
- Auto-registration: When the extension loads, it immediately registers the session in the
agent_sessionstable with its workspace name (derived from the git repo root). This happens at load time — not when the first user message arrives — so the agent is visible in the mesh right away. - Heartbeat: Every 10 seconds, each session updates its heartbeat. Sessions with no heartbeat for 10+ minutes are marked
stopped. - Message passing: Sessions insert messages into
agent_messages. The recipient's polling loop picks them up and routes them viasession.send()for the LLM to process. - Threading: Replies are linked to original messages via
original_message_id. Follow-up messages within 10 minutes are auto-threaded. - Cleanup: Read messages older than 24 hours are purged. Unread messages to stopped sessions older than 24 hours are also purged.
- GitHub Copilot CLI installed and working
- Node.js 22+ (required for
node:sqlite/DatabaseSync)
-
Create the extension directory:
mkdir -p ~/.copilot/extensions/agent-mesh -
Copy the extension file:
cp extension.mjs ~/.copilot/extensions/agent-mesh/extension.mjsOr clone this repo directly:
git clone https://github.com/htekdev/agent-mesh.git ~/.copilot/extensions/agent-mesh -
Restart your Copilot CLI sessions. The extension loads automatically.
That's it. No npm install, no config files, no environment variables. The SQLite database is created automatically at ~/.copilot/extensions/agent-mesh/agent-mesh.db.
After restarting, you should see in the CLI output:
🌐 Agent mesh: registered as "my-repo" — polling every 10s (early registration)
And the get_agents, send_message, reply_to_message, and get_message tools will be available.
List all sessions registered in the mesh.
| Parameter | Type | Required | Description |
|---|---|---|---|
status |
string |
No | Filter: active, stopped, or all (default: all) |
Example output:
🌐 Agent Mesh — 3 session(s):
🟢 my-frontend ← YOU
Session: abc-123-...
Repo: my-frontend
Status: active
Description: React frontend application
🟢 my-api
Session: def-456-...
Repo: my-api
Status: active
Description: Express API server
⚫ infra
Session: ghi-789-...
Repo: infra
Status: stopped
Last heartbeat: 2026-05-01T10:00:00Z
Send a message to another session. Target by workspace name (preferred) or session ID.
| Parameter | Type | Required | Description |
|---|---|---|---|
workspace |
string |
No* | Target workspace/repo name (e.g., my-api) |
recipient_session_id |
string |
No* | Target session ID (from get_agents) |
content |
string |
Yes | The message to send (max 10KB) |
priority |
string |
No | low, normal (default), high, or urgent |
* Must provide either workspace or recipient_session_id.
Example:
send_message(workspace="my-api", content="What endpoints handle user authentication?", priority="normal")
Reply to a received message. Creates a threaded response.
| Parameter | Type | Required | Description |
|---|---|---|---|
message_id |
integer |
Yes | The message ID to reply to |
content |
string |
Yes | Your reply (max 10KB) |
priority |
string |
No | Override priority (default: same as original) |
Retrieve a specific message and any replies to it.
| Parameter | Type | Required | Description |
|---|---|---|---|
message_id |
integer |
Yes | The message ID to retrieve |
You're working in my-frontend and hit an API error. Ask the API agent:
User: "I'm getting a 403 on /api/users. Can you ask the API agent what middleware checks that route?"
→ send_message(workspace="my-api", content="What authentication middleware guards the /api/users endpoint? The frontend is getting 403 errors.")
The API session receives the message, analyzes its codebase, and sends a reply back through the mesh.
You need to update a shared type definition that both repos use:
User: "Tell the API agent to update the UserProfile type to include avatarUrl"
→ send_message(workspace="my-api", content="Please add 'avatarUrl: string' to the UserProfile type in src/types/user.ts. The frontend needs it for the new profile page.", priority="high")
After sending a message, check if you got a reply:
→ get_message(message_id=42)
📨 Message 42:
From: my-frontend
Content: What auth middleware guards /api/users?
📩 Replies (1):
[43] my-api: The /api/users endpoint uses authMiddleware from src/middleware/auth.ts...
agent_sessions — Registry of all CLI sessions
| Column | Type | Description |
|---|---|---|
session_id |
TEXT (PK) | UUID assigned per CLI session |
agent_name |
TEXT | Workspace/repo name |
agent_description |
TEXT | First line of .github/copilot-instructions.md |
cwd |
TEXT | Working directory |
repo |
TEXT | Git repo folder name |
status |
TEXT | active or stopped |
registered_at |
TEXT | ISO 8601 timestamp |
last_heartbeat |
TEXT | ISO 8601 timestamp |
metadata |
TEXT | Reserved for future use |
agent_messages — Message queue
| Column | Type | Description |
|---|---|---|
message_id |
INTEGER (PK) | Auto-incrementing ID |
sender_session_id |
TEXT (FK) | Sender's session ID |
recipient_session_id |
TEXT (FK) | Recipient's session ID |
content |
TEXT | Message body (max 10KB) |
original_message_id |
INTEGER (FK) | Parent message for threading |
priority |
TEXT | low, normal, high, urgent |
created_at |
TEXT | ISO 8601 timestamp |
read |
INTEGER | 0 = unread, 1 = read |
read_at |
TEXT | When the message was read |
expires_at |
TEXT | Optional TTL |
Sender SQLite DB Recipient
│ │ │
│ INSERT INTO agent_messages │ │
│ ─────────────────────────────>│ │
│ │ Poll (every 10s) │
│ │<───────────────────────────────│
│ │ SELECT unread WHERE │
│ │ recipient = me │
│ │───────────────────────────────>│
│ │ │
│ │ session.send(prompt) │
│ │ ─── LLM processes ─── │
│ │ │
│ │ Mark read │
│ │<───────────────────────────────│
│ │ │
│ │ INSERT reply │
│ │<───────────────────────────────│
│ Poll picks up reply │ │
│<───────────────────────────────│ │
- Rate limiting: Max 10 messages between any pair of sessions within 60 seconds
- Message size limit: 10KB per message
- Self-message prevention: Cannot send messages to yourself
- Stale session cleanup: Sessions with no heartbeat for 10+ minutes are marked stopped
- Message TTL: Read messages are purged after 24 hours
- Queue depth warning: Console warning when >50 unread messages queue up
- Graceful degradation: If
node:sqliteis unavailable (Node < 22), the extension loads as a no-op stub with a clear error message
The extension has sensible defaults and requires no configuration. If you need to customize, edit these constants at the top of extension.mjs:
| Constant | Default | Description |
|---|---|---|
POLL_INTERVAL_MS |
10000 |
How often to check for messages (ms) |
MAX_MESSAGES_PER_POLL |
5 |
Max messages processed per poll cycle |
MAX_MESSAGE_SIZE |
10240 |
Max message content size (bytes) |
The database path is automatically set to agent-mesh.db in the same directory as the extension file.
The extension uses Copilot CLI extension hooks for lifecycle management and context injection:
| Hook | Purpose |
|---|---|
| (load time) | Auto-register, clean stale sessions, start polling (runs immediately when extension loads) |
onSessionStart |
Re-register if cwd differs from initial detection, inject peer list into context |
onSessionEnd |
Stop polling, mark session as stopped |
onPostToolUse |
Inject contextual guidance after tool calls (e.g., "don't wait for replies, they arrive automatically") |
Sessions can be addressed in two ways:
- By workspace name (recommended):
send_message(workspace="my-api", ...)— resolves to the most recently active session in that workspace. Stable across restarts. - By session ID:
send_message(recipient_session_id="abc-123-...", ...)— exact targeting. Session IDs change every time the CLI restarts.
The workspace name is derived from the git repository root folder name. If you have ~/repos/my-api/.git, the workspace is my-api.
Each session's description is auto-derived from the first non-empty line of .github/copilot-instructions.md in the repo. This helps other agents understand what each session does when they call get_agents.
If no instructions file exists, the workspace name is used as the description.
- Single machine only: The SQLite database is local — no network/cloud support. All sessions must run on the same machine.
- No authentication: All sessions on the same machine are trusted equally. This is a single-user tool.
- No persistence across reboots: Session registrations are cleaned up when sessions stop. Messages older than 24h are purged.
- Node.js 22+ required: Uses
node:sqlite(DatabaseSync) which is only available in Node 22+.
Q: Do I need to install any npm packages?
No. The extension uses only Node.js built-in modules (node:sqlite, node:path, node:fs, node:url) and the @github/copilot-sdk/extension package that ships with Copilot CLI.
Q: What happens if the recipient session is offline? The message is stored in the database. If the session comes back online within 24 hours, it will pick up the message. After 24 hours, unread messages to stopped sessions are purged.
Q: Can I use this with agents in different programming languages? Yes — each Copilot CLI session is language-agnostic. The mesh doesn't care what language the repo uses. A Python project's agent can talk to a TypeScript project's agent.
Q: How do I see who's online?
Use the get_agents(status="active") tool in any Copilot CLI session.
Q: What if two terminals are open in the same repo?
The workspace-based addressing (send_message(workspace="...")) resolves to the most recently active session. If you need to target a specific terminal, use recipient_session_id instead.
Issues and PRs welcome. This is a simple, single-file extension — the entire implementation is in extension.mjs.