Skip to main content

Multi-Session Coordination Guide

How to coordinate multiple LLM sessions (Claude, Codex, Gemini, Kimi) working on the same project using the Inter-Session Message Bus.


Overview

When multiple LLM sessions work on the same project simultaneously, they need coordination to avoid:

  • File overwrites - Two sessions editing the same file
  • Task duplication - Two sessions working on the same task
  • Context blindness - Sessions unaware of each other's progress

The Inter-Session Message Bus (messaging.db) solves this with four mechanisms:

MechanismPurposeTable
Session RegistryTrack active LLM sessionssession_registry
Pub/Sub MessagingBroadcast task status updatesinter_session_messages
File LocksAdvisory file-level conflict preventionfile_locks
Task ClaimsExclusive task ownershiptask_claims

Quick Start

1. Register Your Session

/session-register

This registers the current LLM session in messaging.db with:

  • LLM vendor and model (e.g., claude opus-4.6)
  • Process ID and project
  • Starts a background heartbeat thread (every 30s)

2. Check Who Else Is Working

/session-status

Shows all active sessions across all LLM vendors:

Cross-LLM Session Dashboard
==================================================
2 active sessions | 1 file lock | 1 task claim

Session: claude-29583
Vendor: claude (opus-4.6)
Project: PILOT
Task: H.13.7

Session: codex-41200
Vendor: codex (o3)
Project: PILOT
Task: J.27.4

3. Lock Files Before Editing

/session-conflicts --lock path/to/file.py

If the file is already locked by another session, you'll see a conflict warning.

4. Claim Tasks for Exclusive Ownership

# Via Python API
from scripts.core.session_message_bus import get_session_message_bus
bus = get_session_message_bus()
claimed = bus.claim_task("H.13.7", session_id="claude-29583")
# True if claimed, False if already claimed by another active session

Architecture

Database: messaging.db

Located at ~/.coditect-data/context-storage/messaging.db. Uses SQLite WAL mode for high concurrency.

messaging.db
├── session_registry # Active LLM sessions
├── inter_session_messages # Pub/sub channel messages
├── file_locks # Advisory file locks
└── task_claims # Exclusive task ownership

Why Not sessions.db?

messaging.db is a dedicated lightweight database (~50-130 KB) optimized for high-frequency concurrent access. Keeping it separate from sessions.db (which can grow to 18+ GB) ensures messaging operations stay fast.


Core Concepts

Session Registry

Every LLM session registers itself on start and unregisters on exit.

bus.register_session(
session_id="claude-29583",
llm_vendor="claude",
llm_model="opus-4.6",
project_id="PILOT",
pid=29583
)

Heartbeats: A background thread sends heartbeats every 30 seconds. Sessions that miss heartbeats beyond the stale timeout (default: 300s) are automatically cleaned up, along with their file locks and task claims.

Pub/Sub Messaging

Sessions can broadcast and receive messages on named channels:

# Publish a message
bus.publish("task_broadcast", {"task_id": "H.13.7", "action": "started"})

# Poll for new messages
messages = bus.poll("task_broadcast", since_id=0)
for msg in messages:
print(f"{msg.sender}: {msg.payload}")

Built-in channels:

ChannelTTLPurpose
task_broadcast600sTask status announcements
file_conflict300sFile conflict warnings
state300sGeneral state updates

File Locks (Advisory)

File locks are advisory - they warn but don't block. Sessions should check before editing.

# Acquire lock
acquired = bus.lock_file("src/main.py", session_id="claude-29583")
if acquired:
# Safe to edit
pass
else:
# Another session holds the lock - coordinate!
pass

# Release when done
bus.unlock_file("src/main.py", session_id="claude-29583")

Behaviors:

  • Same session can re-lock a file it already holds (re-entrant)
  • Locks from unregistered/stale sessions can be taken over
  • unregister_session() releases all locks for that session

Task Claims (Exclusive)

Task claims provide exclusive ownership - only one active session can claim a task.

# Claim a task
claimed = bus.claim_task("H.13.7", session_id="claude-29583")
# Returns True if claimed, False if already claimed by another active session

# Release when done
bus.release_task("H.13.7", session_id="claude-29583")

Behaviors:

  • Claiming automatically broadcasts a "started" message
  • Releasing automatically broadcasts a "released" message
  • Claims from stale sessions can be taken over
  • Race condition safe (uses UNIQUE constraint + IntegrityError handling)

Cross-LLM Status

See what all LLM vendors are doing across the project:

status = bus.get_cross_llm_status()
for s in status:
print(f"{s['llm_vendor']} ({s['llm_model']}): task={s['task_id']}, claim={s['claimed_task']}")

Returns a list of dicts with session_id, llm_vendor, llm_model, project_id, task_id, pid, heartbeat_at, and claimed_task.


Common Workflows

Workflow 1: Two LLMs on Separate Tracks

Terminal 1 (Claude - Track H):
/session-register --task-id H.13.7
bus.claim_task("H.13.7")
bus.lock_file("scripts/core/session_message_bus.py")
# ... work on H.13 ...
bus.unlock_file("scripts/core/session_message_bus.py")
bus.release_task("H.13.7")

Terminal 2 (Codex - Track J):
/session-register --task-id J.27.4
bus.claim_task("J.27.4")
bus.lock_file("scripts/core/paths.py")
# ... work on J.27 ...
bus.unlock_file("scripts/core/paths.py")
bus.release_task("J.27.4")

No conflicts because they're working on different files and tasks.

Workflow 2: Conflict Detection

Terminal 1 (Claude):
bus.lock_file("src/shared.py")
# ... editing ...

Terminal 2 (Codex):
acquired = bus.lock_file("src/shared.py")
# acquired = False!
# Check who holds the lock:
locks = bus.get_file_locks()
# → claude-29583 holds src/shared.py
# Coordinate before proceeding

Workflow 3: Task Handoff

Terminal 1 (Claude - finishes H.13.6):
bus.release_task("H.13.6")
bus.broadcast_task("H.13.6", "completed", {"outcome": "success"})

Terminal 2 (Codex - picks up H.13.7):
messages = bus.poll("task_broadcast")
# Sees: H.13.6 completed
bus.claim_task("H.13.7")
# ... continues the work ...

Project Discovery Integration

The message bus integrates with discover_project() in paths.py. When multiple projects match the current working directory:

  1. Bus lock-out (Step 1a): Projects with active LLM sessions (from messaging.db) are filtered out
  2. Process detection (Step 1b): Fallback using pgrep/lsof for sessions not registered in the bus
  3. Activity ranking (Step 2): Remaining projects ranked by most recent activity

This prevents a new LLM session from accidentally selecting a project that another session is already working on.


Performance

Benchmarked with 10 concurrent sessions:

Operationp50p99Max
Publish message1.2ms41.6ms85ms
Poll messages0.1ms0.3ms1.2ms
Claim task0.8ms3.9ms12ms
Register session0.5ms2.1ms8ms

Database size: ~128 KB with 500 messages and 10 sessions. Throughput: ~4,000 messages/second.


Troubleshooting

Session Shows as Stale

Cause: Heartbeat thread stopped (session crashed or context compacted). Fix: Re-register with /session-register.

Lock Won't Release

Cause: Session that held the lock is gone. Fix: Register a new session and attempt to lock the file - stale locks are automatically taken over.

Task Already Claimed

Cause: Another active session holds the claim. Fix: Check /session-status --claims to see who holds it, then coordinate.

Database Locked

Cause: Heavy concurrent access. Fix: The bus uses busy_timeout=3000ms with automatic retry. If persisting, check for long-running transactions.


API Reference

SessionMessageBus Methods

MethodPurpose
register_session(session_id, llm_vendor, llm_model, project_id, pid)Register a session
unregister_session(session_id)Unregister (releases locks and claims)
heartbeat(session_id)Update heartbeat timestamp
start_heartbeat_thread(interval, session_id)Start background heartbeat
list_sessions(active_only)List registered sessions
publish(channel, payload, ttl_seconds)Publish message to channel
poll(channel, since_id, limit)Poll messages from channel
lock_file(file_path, session_id)Acquire advisory file lock
unlock_file(file_path, session_id)Release file lock
get_file_locks()List all active file locks
claim_task(task_id, session_id)Claim exclusive task ownership
release_task(task_id, session_id)Release task claim
get_task_claims()List all active task claims
broadcast_task(task_id, action, details)Broadcast task status
update_session_task(task_id, session_id)Update session's current task
get_cross_llm_status()Get all sessions with claims
stats()Get bus statistics

Factory Function

from scripts.core.session_message_bus import get_session_message_bus

# Uses default messaging.db path
bus = get_session_message_bus()

# Custom path
bus = get_session_message_bus(db_path=Path("/custom/path/messaging.db"))

ResourcePath
Python APIscripts/core/session_message_bus.py
Testsscripts/tests/test_session_message_bus.py (35 tests)
ADR-160internal/architecture/adrs/ADR-160-inter-session-messaging.md
Setupscripts/CODITECT-CORE-INITIAL-SETUP.py (Step 4)
/session-registercommands/session-register.md
/session-statuscommands/session-status.md
/session-conflictscommands/session-conflicts.md

Version: 1.0.0 Created: 2026-02-06 Author: Claude (Opus 4.6) Track: H (Framework Autonomy) Task: H.13.7.3