Build a Cross-Session Agent Coordination Workflow with Claude Code Session Messaging
Anthropic's Claude Code v2.1.224 (Aug 2026) lets one session send a composed summary to another mid-task instead of forcing a context re-explain. This article builds the swam version of that primitive: job-scoped session topology, explicit context budgets, a typed handoff schema, a deduping message ledger, retry/idempotency rules, and a hard no-privileged-forwarding policy for macOS and Linux.
Deepak Bagada
CEO, SaaSNext
- v2.1.224 cross-session messaging forwards a model-composed summary from a short hint, never raw history; privileged actions still prompt the receiving session.
- One session per job role plus a coordinator keeps message legality auditable at the ledger layer before any model sees a message.
- Defend the invariant session_tokens + pending_messages <= budget_ceiling; retire and relaunch sessions at 80 percent occupancy.
- Idempotent retries come from INSERT OR IGNORE on a stable message_id; retries resend the composed handoff, never the session's raw history.
- The security contract is no privileged forwarding: approvals and config changes do not forward, and every send lands in an append-only audit trail.
Build a Cross-Session Agent Coordination Workflow with Claude Code Session Messaging
On August 12, 2026, Anthropic shipped Claude Code v2.1.224, and with it a feature that quietly changes how multi-agent workflows get built: cross-session messaging. One running Claude Code session can now send a summary to another session mid-task — not a raw transcript dump, but a message Claude composes from a short user hint. The receiving session folds that handoff into its own scratchpad and keeps working. What makes this coordination-safe rather than chat-y is the list of exclusions: permission approvals and config changes do not forward, and privileged actions still prompt the receiving session the moment they are about to run. It is the difference between "share my tab" and "share my intent with guardrails."
This article turns that primitive into a production-grade workflow: a multi-session swarm — one planner/coordinator session plus N job-scoped worker sessions — exchanging handoff me-sagers, bounded by explicit context budgets, validated against a typed schema, retried with idempotency, and locked down with a "no privileged forwarding" rule. Everything runs on macOS or Linux, the two platforms the feature ships on.
Why Cross-Session Messaging Beats Re-Explaining Context
Before v2.1.224, the standard way to hand work between two Claude Code sessions was to embed context in the prompt of the new session: copy summaries, paste diffs, re-state constraints. That is slow, lossy, and — critically — it couples the two sessions by translation, so drift accumulates. Cross-session messaging changes the coupling model:
- Composed, not copied. The sending session's model compresses its working state into a handoff message guided by a one-line hint (for example
handoff what you learned to the worker running job-44). The receiving session gets intent plus state, not a token dump. - No raw history dump. History is excluded from forwarding by design, so the target session's context window is never polluted with tool-call logs it does not need.
- Privilege exclusions built in. Permission approvals and config changes are not part of a message; session-scoped permissions remain the only authority for running privileged commands.
- Explicit re-prompting. When a handoff implies a privileged step, the receiving session is still prompted on its own terminal. The message reduces the distance between sessions but never removes the consent boundary.
Those four properties are why the workflow in this guide scales past a handful of sessions: coordination becomes a protocol with a typed envelope, not a copy-paste habit.
The Architecture: Job-Scoped Session Topology
The swarm uses one session per job role. A coordinator session plans, allocates, and supervises; worker sessions each own one repository slice and exchange messages only with the coordinator (plus narrow direct lanes when their contracts genuinely overlap). The topology is the first thing you design, because it decides which messages are legal.
graph TD
ORC[Coordinator Session / planner + allocator] -->|handoff me-sager / JOB-START-0| W1[Worker A / frontend slice]
ORC -->|handoff me-sager / JOB-START-1| W2[Worker B / backend slice]
ORC -->|handoff me-sager / JOB-START-2| W3[Worker C / tests + review]
W3 -->|handoff me-sager / PROGRESS->diff-Z| ORC
W1 -->|handoff me-sager / PROGRESS->contract-ping| ORC
W2 -->|direct lane / schema change -> notify| W1
subgraph LEDG[Message Ledger / SQLite + append-only jsonl]
L1[(dedupe by message_id / acks / retries)]
end
ORC -.persist.-> LEDG
W1 -.persist.-> LEDG
subgraph VAULT[Secret Vault / never in message payloads]
V1[(scoped creds / read-only for sessions)]
end
W1 -.read-only.-> VAULT
W2 -.read-only.-> VAULT
Every arrow above is a handoff me-sager: the native v2.1.224 messaging primitive wrapped in a small Python transport that adds ack, dedupe, and retry on top. The design rules are:
| Session | Role | Budget ceiling | Message types it may send |
|---|---|---|---|
| coordinator | Plans, allocates, tracks state | 96k tokens | JOB-START, QUERY, STOP |
| worker-frontend | One repo slice (frontend) | 128k tokens | PROGRESS, HANDOFF, REQUEST |
| worker-backend | One repo slice (backend) | 128k tokens | PROGRESS, HANDOFF, REQUEST |
| worker-qa | Test/validation on merged slices | 64k tokens | RESULT, REQUEST |
Sessions are launched per job with a strict prefix naming scheme (job-44-frontend, job-44-qa), and each carries its job ID in the environment so every message, budget decision, and audit row traces to a unit of work. Messages that name a target session outside the sender's assigned job are rejected at the ledger layer before any model sees them.
Context Budgets: The Real Currency
Each session's own context window is its working set; handoff messages are the exchange medium; the ledger is the durable record. The invariant to defend:
session_tokens + pending_messages_for_session <= budget_ceiling
If pending handoff messages pile up faster than a worker drains them, three bad things happen at once: the worker thrashes its context re-summarizing recent arrivals, the coordinator's window fills with acks, and retries become ambiguous. The budget rules that keep the swarm stable:
- Every incoming message reserves a fixed header slot (roughly 400 tokens) in the target session's window; the coordinator flags any session with more than 5 pending messages.
- Workers and the coordinator dispose of drained messages — deliver, ack, then drop the body from context, keeping only the schema-level record in the ledger.
- The coordinator never sends a new JOB-START until the target's projected occupancy (reported tokens plus all unacked sends) clears 60% of its ceiling.
Environment configuration lives in .env:
# .env — swarm topology, budgets, transport
CLAUDE_TOPOLOGY=coordinator,frontend,backend,qa
COORDINATOR_BUDGET=96000
WORKER_BUDGET=128000
QA_BUDGET=64000
MESSAGE_LEDGER=./ledger/messages.sqlite3
AUDIT_TRAIL=./ledger/audit.jsonl
SWARM_HOME=./swarm
SESSION_MESSAGING=1
MESSAGE_RETRY_MAX=3
MESSAGE_RETRY_BACKOFF=2.0
HANDOFF_HINT_MAXLEN=120
BUDGET_RESERVE_FRACTION=0.8
PRIVILEGED_FORWARD_FORBIDDEN=1
The Handoff Message Schema
Every message is typed, versioned, and validated before a model touches it. messaging/schemas.py:
# messaging/schemas.py
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field, field_validator
class MessageType(str, Enum):
JOB_START = "job.start"
PROGRESS = "job.progress"
HANDOFF = "coord.handoff"
REQUEST = "coord.request"
RESULT = "job.result"
QUERY = "coord.query"
STOP = "coord.stop"
class HandoffMessage(BaseModel):
message_id: str = Field(pattern=r"^msg_[0-9a-f]{16}$")
job_id: str
source_session: str
target_session: str
type: MessageType
hint: str = Field(max_length=120)
summary: str = Field(max_length=4000)
payload_ref: str | None = None # path to an artifact on disk, never the artifact body
parent_id: str | None = None # links a retry back to its original message
intent_directives: list[str] = []
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@field_validator("hint")
@classmethod
def hint_must_be_a_hint(cls, v: str) -> str:
if "full log" in v.lower() or "dump everything" in v.lower():
raise ValueError("hint must ask for composed coordination, not a raw dump")
return v
@field_validator("summary")
@classmethod
def no_privileged_payload(cls, v: str) -> str:
if "PERMISSION" in v.upper() or "APPROVED=" in v.upper():
raise ValueError("approvals are excluded by design from forwarded content")
return v
The schema is also the security surface: it has no credential fields, payload_ref points at a path rather than embedding content, and intent_directives is the only free-form expansion hat. Anything a session needs that is not in this schema must come through a separate, permissioned channel — never through a message.
The Transport and Tool Layer
tools.py wraps the native primitive with transport that a session model can call, and pushes dedupe, ack, and retry to the edges:
# tools.py
import json
import sqlite3
import time
from schemas import HandoffMessage, MessageType
DB = "ledger/messages.sqlite3"
CREATE_LEDGER = "CREATE TABLE IF NOT EXISTS messages (message_id TEXT PRIMARY KEY, job_id TEXT, source TEXT, target TEXT, mtype TEXT, summary TEXT, payload_ref TEXT, parent_id TEXT, state TEXT, attempts INT, acked_at TEXT, created_at TEXT);"
LONG_TIMEOUT_S = 900
def open_ledger():
conn = sqlite3.connect(DB)
conn.execute(CREATE_LEDGER)
return conn
def send_message(msg: HandoffMessage, native_transport) -> dict:
conn = open_ledger()
conn.execute(
"INSERT OR IGNORE INTO messages VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(msg.message_id, msg.job_id, msg.source_session, msg.target_session,
msg.type.value, msg.summary, msg.payload_ref, msg.parent_id,
"PENDING", 0, None, msg.created_at.isoformat()),
)
conn.commit()
# native_transport invokes the v2.1.224 messaging primitive on macOS/Linux.
# The receiving session's model composes the actual handoff summary from
# msg.hint plus msg.intent_directives; history is never forwarded.
return native_transport(target=msg.target_session, hint=msg.hint,
directives=msg.intent_directives,
payload_ref=msg.payload_ref)
def ack(message_id: str) -> None:
conn = open_ledger()
conn.execute("UPDATE messages SET state='ACKED', acked_at=? WHERE message_id=?",
(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), message_id))
conn.commit()
def pending_for(target: str) -> list[dict]:
conn = open_ledger()
rows = conn.execute(
"SELECT * FROM messages WHERE target=? AND state='PENDING' ORDER BY created_at",
(target,)).fetchall()
cols = ["message_id", "job_id", "source", "target", "mtype", "summary",
"payload_ref", "parent_id", "state", "attempts", "acked_at", "created_at"]
return [dict(zip(cols, r)) for r in rows]
def resend(message_id: str) -> None:
conn = open_ledger()
conn.execute("UPDATE messages SET attempts = attempts + 1 WHERE message_id=?", (message_id,))
conn.commit()
def retry_backoff(attempt: int) -> float:
return min(MESSAGE_RETRY_BACKOFF ** attempt, 60.0)
Two details matter in production: INSERT OR IGNORE gives idempotency by construction (a re-sent message_id is a durable no-op), and the transport is the only place the native primitive is touched — retries, acks, and dedupe live around it, so the actual call to the session-messaging layer stays one-shot.
The Orchestrator
orchestrator/graph.py is a small state machine over the ledger. It does not run inside a model; it runs as the supervising process that spawns, budgets, and retires the target sessions:
# orchestrator/graph.py
import os
import uuid
from schemas import HandoffMessage, MessageType
from tools import send_message, resend, pending_for
class SwarmGraph:
def __init__(self, topology: dict[str, int]):
self.topology = topology # role -> budget ceiling
self.occupancy: dict[str, int] = {} # reported live tokens per session
self.coordinator = "coordinator"
def allocate(self, job_id: str, plan: dict[str, list[str]]):
for role, slices in plan.items():
msg = HandoffMessage(
message_id=f"msg_{uuid.uuid4().hex[:16]}",
job_id=job_id,
source_session=self.coordinator,
target_session=f"{job_id}-{role}",
type=MessageType.JOB_START,
hint="Start on your slice; reply with a plan before editing.",
summary=f"Allocated {len(slices)} path slices to role {role}.",
intent_directives=[f"read-only-on:{s}" for s in slices],
)
send_message(msg, self._native_transport())
def observe(self, job_id: str):
for row in pending_for(job_id):
self._enforce_budget(row["target"])
return self._converged(job_id)
def _enforce_budget(self, target: str):
role = target.rsplit("-", 1)[-1]
ceiling = self.topology.get(role, 96000)
reserve = float(os.getenv("BUDGET_RESERVE_FRACTION", "0.8"))
if self.occupancy.get(target, 0) > int(ceiling * reserve):
self._stop_and_reset(target)
def _stop_and_reset(self, target: str):
send_message(self._stop_msg(target), self._native_transport())
# ledger snapshot becomes the resume point for a fresh session
def _stop_msg(self, target: str) -> HandoffMessage:
return HandoffMessage(
message_id=f"msg_{uuid.uuid4().hex[:16]}",
job_id="-", source_session=self.coordinator, target_session=target,
type=MessageType.STOP,
hint="Retire now; write a compact state snapshot to the ledger.",
summary="Budget ceiling crossed at 80 percent.",
)
def _native_transport(self):
raise NotImplementedError("bind the v2.1.224 messaging primitive here")
def _converged(self, job_id: str) -> bool:
active = [r for r in pending_for(job_id) if r["mtype"] == "job.progress"]
return len(active) == 0
main.py ties the swarm together and is the only file a human or CI pipeline runs. It validates topology from env, bootstraps the ledger, allocates jobs, then enters a supervise loop that budget-checks, resends stalled messages with backoff, and reports per-session state:
# main.py
import os
import time
from messaging.schemas import MessageType
from messaging.tools import pending_for, resend, retry_backoff
from orchestrator.graph import SwarmGraph
TOPOLOGY = {"coordinator": 96000, "frontend": 128000, "backend": 128000, "qa": 64000}
MAX_ATTEMPTS = int(os.getenv("MESSAGE_RETRY_MAX", "3"))
def supervise():
graph = SwarmGraph(TOPOLOGY)
job_id = os.getenv("JOB_ID", f"job-{int(time.time())}")
plan = {
"frontend": ["app/web/*", "app/ui/*"],
"backend": ["services/api/*", "services/core/*"],
"qa": ["tests/**", "perf_scripts/*"],
}
graph.allocate(job_id, plan)
while not graph.observe(job_id):
for row in pending_for(job_id):
if int(row["attempts"]) >= MAX_ATTEMPTS:
notify_operator(f"stalled: {row['message_id']}")
continue
time.sleep(retry_backoff(int(row["attempts"])))
resend(row["message_id"])
time.sleep(5)
print(f"[swarm] job {job_id} converged")
if __name__ == "__main__":
supervise()
Retry, Idempotency, and Error-Handling Rules
The ledger is the source of truth, and these rules keep retries honest:
| Failure | Detection | Behavior |
|---|---|---|
| Transport error on send | send_message raises |
Keep the PENDING row, back off 2^n seconds (cap 60s), resend with the same message_id |
| Receiver never acks | ACK timeout > 60s | Coordinator sends QUERY; if still silent, restart the target from its ledger snapshot and re-send once |
| Duplicate delivery | INSERT OR IGNORE |
No-op by construction; ack the duplicate, never double-run the job |
| Session over budget | Probe reads cross reserve | STOP-and-sync: snapshot state, retire the session, relaunch resumed |
| Message schema invalid | Pydantic validation | Drop the message, write the failure to the audit trail, page the coordinator |
| Privileged step implied | Receiving session prompts | Message is not auto-forwarded; a human accepts or denies in the receiving session |
The rule that deserves emphasis: retries re-send the composed handoff, never the raw context. If a session's own history is what failed to deliver, you restart that session from the ledger snapshot — you do not dump history across the wire.
Security Rules: No Privileged Forwarding
Cross-session messaging is coordination, not delegation of consent. The lock-down list:
- Privileged actions are never forwarded. If composing a handoff implies a permission-gated command (a push, a production write, a vault read), the receiving session still prompts its own human in its own terminal. The message can carry intent; it cannot carry authority.
- Permission approvals and config changes are excluded from what forwards by the v2.1.224 design. Treat any attempt to encode an approval inside
summaryas a violation — the schema rejects it and the audit rows make it visible. - No credentials in messages. The schema has no secret fields, payload references are paths, and the vault is only ever read directly by sessions, never via a forwarded message.
- Per-job session identity. A worker can only address target sessions inside its own
job_id; the ledger rejects cross-job writes before any model sees a message. - Audit every send.
audit.jsonlrecordsmessage_id, source, target, type, payload_ref, attempts, and terminal state — so a post-incident review can prove which session composed which handoff and whether any privileged step ever crossed a session boundary (it should not).
Add a human-in-the-loop gate at the coordinator as well: any JOB-START that expands a session's path slices beyond what is in intent_directives requires manual confirmation. Coordination should reduce re-explaining, never reduce consent.
Observability
Every message is a perfect event to watch: type, latency to ack, attempts before success, and per-session budget pressure over time. Track handoff compose time (how long the sending model took to compress state into a summary), drain rate (messages acked per minute per session), and retry rate (messages delivered on attempt 2 or later). A rising compose time is usually the first sign that a session has crossed its real cognitive budget even when it is still technically under the token ceiling. Cross-reference budget burn with the worker-role maps in our AI Workflows library, wire message shipping into your tracing stack through transport servers in the MCP Directory, and keep one eye on the latest AI news feed, because Anthropic has been tightening the messaging contract every few patch releases.
Wrap-up
Claude Code v2.1.224's cross-session messaging hands agent engineers a genuinely new channel: sessions exchange composed intent — with compose-on-send, exclusions on privileges, and prompts at the boundary. When you add job-scoped topology, explicit context budgets, a typed schema, a deduping ledger, and a "no privileged forwarding" rule, the primitive becomes a swarm protocol that scales without turning context into a landfill. The cost of all this coordination is zero trust: every message is an event, every session is a scope, and every handoff is still a prompt — not a permission. Build the swarm, keep the retries idempotent, and never let a summary carry more authority than the human who signed off on it.
For more agent-orchestration patterns, browse the AI Workflows archive, find transport and telemetry tooling in the MCP Directory, and follow the latest AI news for every Claude Code patch that touches the messaging contract.
Frequently Asked Questions
How does a session actually "send" a message to another session in v2.1.224? Through the native cross-session messaging primitive: you supply a short hint and optionally a payload reference, and the sending session's model composes a concise handoff summary. The receiving session receives that composed summary, not your transcript.
What kinds of messages are excluded by the feature's design? Permission approvals and config changes do not forward. And when a handoff implies a privileged action, the receiving session still shows its own permission prompt — no message can silently authorize a command.
Do retries risk double-executing a job?
No. Every message carries a stable message_id, and the ledger uses INSERT OR IGNORE, so a re-sent message is a no-op at the durable layer. The native transport is a one-shot call; retry logic lives around it, not inside it.
Is cross-session messaging available on every OS? The feature ships on macOS and Linux. Windows support is not part of the release, so this workflow targets the two supported platforms.
Can I bridge sessions across different machines? The primitive targets sessions in the same local agent runtime. If you need cross-machine coordination, treat the ledger as the sync bus and materialize handoff messages into shared state that other machines consume.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
Master 7 Autonomous AI Energy Grid Balancing Workflows in 2026
Next Story →Breaking: Apple Just Announced CoreML-X 100B On-Device AI in 2026
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...