Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ That's it. The Library agent responds with its **full session context** — it k

| Command | Description |
|---------|-------------|
| `/bridge start` | Register this session as a bridge peer |
| `/bridge start [--as <label>]` | Register this session as a bridge peer (label defaults to the git branch) |
| `/bridge connect <id>` | Connect to a peer session (auto-starts if needed) |
| `/bridge listen` | Enter listening mode — answer peer queries continuously |
| `/bridge ask <question>` | Send a question and wait for the response |
Expand All @@ -120,6 +120,10 @@ The key innovation: `/bridge listen` puts the agent into a **continuous listenin
- UUID message IDs — no collision risk
- Connection via ping handshake — peers never mutate each other's manifests

### Multiple sessions in the same repo

Session identity is keyed by the agent session (a per-session pointer derived from the agent process), not the project directory — so two Claude Code sessions in the same repo each get their own bridge identity. They're distinguished by their **label** (the git branch by default, or `--as <label>`) and session ID in `/bridge peers`. `/bridge stop` only cleans up the calling session's own state.

---

## When To Use It
Expand Down Expand Up @@ -191,11 +195,11 @@ Consumer user: "Ask the backend team what the new API response format looks like

```
> /bridge peers
SESSION PROJECT STATUS PATH
------- ------- ------ ----
a1b2c3 auth-sdk active ~/projects/auth-sdk
d4e5f6 payments-service active ~/projects/payments
g7h8i9 my-app active ~/projects/my-app (you)
SESSION PROJECT LABEL STATUS PATH
------- ------- ----- ------ ----
a1b2c3 auth-sdk main active ~/projects/auth-sdk
d4e5f6 payments-service main active ~/projects/payments
g7h8i9 my-app feature/login active ~/projects/my-app (you)

> /bridge ask "What config format does the payments service expect?"
Routes to payments-service peer automatically based on question context
Expand Down Expand Up @@ -261,6 +265,8 @@ plugins/session-bridge/
│ └── SKILL.md # Teaches agent the bridge protocol
├── scripts/
│ ├── register.sh # Create session directory and manifest
│ ├── get-session-id.sh # Resolve this session's ID
│ ├── get-session-key.sh # Resolve stable per-agent-session key
│ ├── send-message.sh # Send message to peer's inbox
│ ├── check-inbox.sh # Scan inboxes for pending messages
│ ├── list-peers.sh # List active sessions
Expand Down
26 changes: 17 additions & 9 deletions plugins/session-bridge/commands/bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,22 @@ Register this session as a bridge peer.
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh"
```
2. Capture the session ID from stdout.
Optionally pass a label to identify this session to peers (defaults to the current git branch):
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/register.sh" --as frontend
```
2. Capture the session ID from stdout. The label is the `--as` value if given, otherwise the current git branch (empty outside a git repo).
3. Display to the user:
```
Bridge active!
Session ID: <session-id>
Label: <label>
Share this ID with other Claude sessions to connect: /bridge connect <session-id>
Use /bridge listen to start receiving and answering peer queries.
```

**Multiple sessions in the same repo are supported.** Each agent session registers independently and is distinguished by its label (git branch by default, or `--as <label>`). Use distinct labels — e.g. `/bridge start --as frontend` and `/bridge start --as backend` — so peers can tell same-repo sessions apart.

### `connect <session-id>`

Connect to a peer session. Auto-starts this session's bridge if not already active.
Expand Down Expand Up @@ -87,7 +94,7 @@ The loop:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/bridge-listen.sh" "$MY_SESSION"
```
3. When a message arrives, parse the output:
- Lines before `---` are metadata (MESSAGE_ID, FROM_ID, TO_ID, FROM_PROJECT, TYPE, IN_REPLY_TO)
- Lines before `---` are metadata (MESSAGE_ID, FROM_ID, TO_ID, FROM_PROJECT, FROM_LABEL, TYPE, IN_REPLY_TO)
- Lines after `---` are the message content

4. Handle by message type. **Use `TO_ID` from the message metadata as your session ID** when sending responses. This is always correct regardless of working directory.
Expand Down Expand Up @@ -123,7 +130,7 @@ Send a query to a connected peer and wait for the response.
```bash
find ~/.claude/session-bridge/sessions/$MY_SESSION/inbox -name "*.json" -exec jq -r 'select(.type == "ping") | .from' {} \; 2>/dev/null | sort -u
```
3. If multiple peers, ask which one to query.
3. If multiple peers, pick the most relevant one by label first, then project name (several sessions may share one project). Ask the user if still ambiguous.
4. Send the query and capture the message ID:
```bash
MSG_ID=$(BRIDGE_SESSION_ID=$MY_SESSION bash "${CLAUDE_PLUGIN_ROOT}/scripts/send-message.sh" "<peer-id>" query "<question>")
Expand All @@ -144,7 +151,7 @@ List all active bridge sessions on this machine.
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/list-peers.sh"
```
2. Display the formatted table.
2. Display the formatted table — its LABEL column shows each session's label (git branch by default, or the `--as` value), which together with the session ID distinguishes multiple sessions in the same repo.
3. To highlight which one is "you", run:
```bash
bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh"
Expand All @@ -159,26 +166,27 @@ Show current bridge state.
MY_SESSION=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-session-id.sh")
```
If it fails, say "Bridge is not active. Run `/bridge start` to begin."
2. Display the session ID.
2. Display the session ID and label (shown in the manifest and in `/bridge peers`).
3. List connected peers (from ping messages in inbox).
4. Count pending (unread) messages in inbox.
5. Count messages in outbox (sent).
6. Display a summary:
```
Bridge Status
Session ID: abc123
Label: frontend
Project: my-app

Connected Peers:
- my-library (def456) - active
- backend (def456) - active

Inbox: 2 pending messages
Outbox: 5 messages sent
```

### `stop`

Unregister and clean up.
Unregister and clean up. This only removes THIS session's own bridge state — other sessions in the same repo (or elsewhere) keep running.

1. Run:
```bash
Expand All @@ -191,11 +199,11 @@ Unregister and clean up.
If no argument is given, show a brief help:
```
Bridge commands:
/bridge start - Register this session
/bridge start [--as label] - Register this session (label defaults to git branch)
/bridge connect <id> - Connect to a peer session
/bridge listen - Listen and answer peer queries (blocks)
/bridge ask <question> - Send a question to a peer
/bridge peers - List active sessions
/bridge peers - List active sessions (with labels)
/bridge status - Show bridge state
/bridge stop - Disconnect and clean up
```
2 changes: 2 additions & 0 deletions plugins/session-bridge/scripts/bridge-listen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ while true; do
MSG_TYPE=$(jq -r '.type' "$MSG_FILE")
CONTENT=$(jq -r '.content' "$MSG_FILE")
FROM_PROJECT=$(jq -r '.metadata.fromProject // "unknown"' "$MSG_FILE")
FROM_LABEL=$(jq -r '.metadata.fromLabel // ""' "$MSG_FILE")
IN_REPLY_TO=$(jq -r '.inReplyTo // ""' "$MSG_FILE")

# Skip messages FROM ourselves (echo prevention)
Expand All @@ -69,6 +70,7 @@ while true; do
echo "FROM_ID=$FROM_ID"
echo "TO_ID=$TO_ID"
echo "FROM_PROJECT=$FROM_PROJECT"
echo "FROM_LABEL=$FROM_LABEL"
echo "TYPE=$MSG_TYPE"
echo "IN_REPLY_TO=$IN_REPLY_TO"
echo "---"
Expand Down
7 changes: 6 additions & 1 deletion plugins/session-bridge/scripts/bridge-receive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@ while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
# Found an unread response!
CONTENT=$(jq -r '.content' "$MSG_FILE")
FROM_PROJECT=$(jq -r '.metadata.fromProject // "unknown"' "$MSG_FILE")
FROM_LABEL=$(jq -r '.metadata.fromLabel // ""' "$MSG_FILE")
MSG_TYPE=$(jq -r '.type' "$MSG_FILE")

# Mark as read
TMP=$(mktemp "$INBOX/$(basename "$MSG_FILE" .json).XXXXXX")
jq '.status = "read"' "$MSG_FILE" > "$TMP"
mv "$TMP" "$MSG_FILE"

echo "Response from $FROM_PROJECT:"
if [ -n "$FROM_LABEL" ]; then
echo "Response from $FROM_PROJECT [$FROM_LABEL]:"
else
echo "Response from $FROM_PROJECT:"
fi
echo "$CONTENT"
exit 0
done
Expand Down
69 changes: 55 additions & 14 deletions plugins/session-bridge/scripts/cleanup.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,57 @@
#!/usr/bin/env bash
# scripts/cleanup.sh — Clean up session on exit. Notify connected peers.
#
# Multi-session aware: resolves THIS agent session's bridge session ID only
# (never a same-repo peer's), notifies its connected peers, removes only its
# own session dir and pointer files, then sweeps stale sessions.
set -euo pipefail

BRIDGE_DIR="${BRIDGE_DIR:-$HOME/.claude/session-bridge}"
PROJECT_DIR="${PROJECT_DIR:-$(pwd)}"
BRIDGE_SESSION_FILE="$PROJECT_DIR/.claude/bridge-session"
LEGACY_POINTER="$PROJECT_DIR/.claude/bridge-session"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Find session ID
SESSION_KEY=$(bash "$SCRIPT_DIR/get-session-key.sh")
POINTER_FILE="$PROJECT_DIR/.claude/bridge-sessions/$SESSION_KEY"

# Find THIS agent session's bridge session ID only — never a same-repo peer's.
# Priority: (a) env override, (b) per-session pointer (walk up from
# PROJECT_DIR, validating the manifest), (c) legacy single pointer,
# (d) manifest scan for projectPath == PROJECT_DIR.
SESSION_ID=""
if [ -f "$BRIDGE_SESSION_FILE" ]; then
SESSION_ID=$(cat "$BRIDGE_SESSION_FILE")
if [ -n "${BRIDGE_SESSION_ID:-}" ] && [ -f "$BRIDGE_DIR/sessions/$BRIDGE_SESSION_ID/manifest.json" ]; then
SESSION_ID="$BRIDGE_SESSION_ID"
else
for MANIFEST_FILE in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST_FILE" ] || continue
MANIFEST_PATH=$(jq -r '.projectPath // ""' "$MANIFEST_FILE" 2>/dev/null)
if [ "$MANIFEST_PATH" = "$PROJECT_DIR" ]; then
SESSION_ID=$(jq -r '.sessionId' "$MANIFEST_FILE")
break
DIR="$PROJECT_DIR"
while true; do
POINTER="$DIR/.claude/bridge-sessions/$SESSION_KEY"
if [ -f "$POINTER" ]; then
SID=$(cat "$POINTER")
if [ -f "$BRIDGE_DIR/sessions/$SID/manifest.json" ]; then
SESSION_ID="$SID"
POINTER_FILE="$POINTER"
break
fi
fi
[ "$DIR" = "/" ] && break
DIR=$(dirname "$DIR")
done
if [ -z "$SESSION_ID" ] && [ -f "$LEGACY_POINTER" ]; then
SID=$(cat "$LEGACY_POINTER")
if [ -f "$BRIDGE_DIR/sessions/$SID/manifest.json" ]; then
SESSION_ID="$SID"
fi
fi
if [ -z "$SESSION_ID" ]; then
for MANIFEST_FILE in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST_FILE" ] || continue
MANIFEST_PATH=$(jq -r '.projectPath // ""' "$MANIFEST_FILE" 2>/dev/null)
if [ "$MANIFEST_PATH" = "$PROJECT_DIR" ]; then
SESSION_ID=$(jq -r '.sessionId' "$MANIFEST_FILE")
break
fi
done
fi
fi

if [ -z "$SESSION_ID" ]; then
Expand All @@ -45,15 +77,24 @@ PEER_IDS=$(echo "$PEER_IDS" | tr ' ' '\n' | sort -u | grep -v '^$' || true)
for PEER_ID in $PEER_IDS; do
if [ -d "$BRIDGE_DIR/sessions/$PEER_ID/inbox" ]; then
BRIDGE_DIR="$BRIDGE_DIR" BRIDGE_SESSION_ID="$SESSION_ID" \
bash "$SCRIPT_DIR/send-message.sh" "$PEER_ID" session-ended "Session ended" 2>/dev/null || true
bash "$SCRIPT_DIR/send-message.sh" "$PEER_ID" session-ended "Session ended" > /dev/null 2>&1 || true
fi
done

# Remove session directory
# Remove only the caller's own session directory
rm -rf "$SESSION_DIR"

# Remove bridge-session pointer
rm -f "$BRIDGE_SESSION_FILE"
# Remove only the caller's own per-session pointer (a same-repo peer has its
# own pointer file under bridge-sessions/ — leave those alone)
if [ -f "$POINTER_FILE" ] && [ "$(cat "$POINTER_FILE")" = "$SESSION_ID" ]; then
rm -f "$POINTER_FILE"
fi

# Remove the legacy pointer only if it points at OUR session (a same-repo
# peer may have written its own ID there before migrating)
if [ -f "$LEGACY_POINTER" ] && [ "$(cat "$LEGACY_POINTER")" = "$SESSION_ID" ]; then
rm -f "$LEGACY_POINTER"
fi

# Clean up stale sessions (heartbeat older than 30 minutes)
STALE_CUTOFF=$(date -u -v-30M +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d "30 minutes ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
Expand Down
7 changes: 6 additions & 1 deletion plugins/session-bridge/scripts/connect-peer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fi

PEER_NAME=$(jq -r '.projectName' "$TARGET_MANIFEST")
PEER_PATH=$(jq -r '.projectPath' "$TARGET_MANIFEST")
PEER_LABEL=$(jq -r '.label // ""' "$TARGET_MANIFEST")

# Check for staleness (>5 min since last heartbeat)
PEER_HB=$(jq -r '.lastHeartbeat' "$TARGET_MANIFEST")
Expand All @@ -30,4 +31,8 @@ fi
BRIDGE_DIR="$BRIDGE_DIR" BRIDGE_SESSION_ID="$SENDER_ID" \
bash "$SCRIPT_DIR/send-message.sh" "$TARGET_ID" ping "connected" > /dev/null

echo "Connected to '$PEER_NAME' ($TARGET_ID) at $PEER_PATH"
if [ -n "$PEER_LABEL" ]; then
echo "Connected to '$PEER_NAME' ($TARGET_ID) [$PEER_LABEL] at $PEER_PATH"
else
echo "Connected to '$PEER_NAME' ($TARGET_ID) at $PEER_PATH"
fi
47 changes: 38 additions & 9 deletions plugins/session-bridge/scripts/get-session-id.sh
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
#!/usr/bin/env bash
# scripts/get-session-id.sh — Reliably find this project's bridge session ID.
# scripts/get-session-id.sh — Reliably find THIS agent session's bridge session ID.
# Works even if the agent cd'd into a subdirectory.
#
# Strategy:
# 1. Try .claude/bridge-session in current directory (fast path)
# 2. Scan all session manifests for one whose projectPath is a parent of $(pwd)
# 1. $BRIDGE_SESSION_ID env, if it points to a live session
# 2. Per-session pointer .claude/bridge-sessions/<session-key>, walking up from cwd
# 3. Legacy single pointer .claude/bridge-session in cwd (pre-multisession installs)
# 4. Scan all session manifests for one whose projectPath is a parent of $(pwd)
# (ambiguous when several sessions share a repo — used only as a last resort)
#
# Outputs: session ID to stdout, or exits 1 if not found.
set -euo pipefail

BRIDGE_DIR="${BRIDGE_DIR:-$HOME/.claude/session-bridge}"
CURRENT_DIR="${PROJECT_DIR:-$(pwd)}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Fast path: .claude/bridge-session in current directory
if [ -f "$CURRENT_DIR/.claude/bridge-session" ]; then
cat "$CURRENT_DIR/.claude/bridge-session"
# 1. Env override (written to CLAUDE_ENV_FILE at register time)
if [ -n "${BRIDGE_SESSION_ID:-}" ] && [ -f "$BRIDGE_DIR/sessions/$BRIDGE_SESSION_ID/manifest.json" ]; then
echo -n "$BRIDGE_SESSION_ID"
exit 0
fi

# Fallback: scan all session manifests for one whose projectPath is a parent of current dir.
# e.g., if we're at /projects/my-lib/src/main/kotlin and a session has
# projectPath=/projects/my-lib, that's a match.
# 2. Per-session pointer, walking up from cwd to find the project root
SESSION_KEY=$(bash "$SCRIPT_DIR/get-session-key.sh")
DIR="$CURRENT_DIR"
while true; do
POINTER="$DIR/.claude/bridge-sessions/$SESSION_KEY"
if [ -f "$POINTER" ]; then
SID=$(cat "$POINTER")
if [ -f "$BRIDGE_DIR/sessions/$SID/manifest.json" ]; then
echo -n "$SID"
exit 0
fi
fi
[ "$DIR" = "/" ] && break
DIR=$(dirname "$DIR")
done

# 3. Legacy single pointer in cwd (backwards compatibility)
if [ -f "$CURRENT_DIR/.claude/bridge-session" ]; then
SID=$(cat "$CURRENT_DIR/.claude/bridge-session")
if [ -f "$BRIDGE_DIR/sessions/$SID/manifest.json" ]; then
echo -n "$SID"
exit 0
fi
fi

# 4. Fallback: scan all session manifests for one whose projectPath is a parent
# of current dir. With multiple sessions in one repo this is ambiguous — the
# per-session pointer above is the reliable path.
for MANIFEST in "$BRIDGE_DIR"/sessions/*/manifest.json; do
[ -f "$MANIFEST" ] || continue
PROJ_PATH=$(jq -r '.projectPath // ""' "$MANIFEST" 2>/dev/null)
Expand Down
35 changes: 35 additions & 0 deletions plugins/session-bridge/scripts/get-session-key.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# scripts/get-session-key.sh — Resolve a stable key identifying the CALLING agent
# session (not the project). Two agent sessions in the same repo get different
# keys; repeated calls from the same agent session get the same key.
#
# Priority:
# 1. $BRIDGE_SESSION_KEY env (explicit override — useful for tests and power users)
# 2. First non-shell ancestor process PID. Inside an agent CLI (e.g. Claude Code),
# tool commands run as descendants of the CLI process, so this PID is stable
# for the lifetime of that session and distinct across sessions.
# 3. Own PID as a last resort (no stable identity available).
#
# Outputs: session key to stdout (e.g. "pid12345" or the override verbatim)
set -euo pipefail

if [ -n "${BRIDGE_SESSION_KEY:-}" ]; then
echo -n "$BRIDGE_SESSION_KEY"
exit 0
fi

PID=$$
while true; do
PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
if [ -z "$PID" ] || [ "$PID" = "0" ] || [ "$PID" = "1" ]; then
break
fi
COMM=$(ps -o comm= -p "$PID" 2>/dev/null || echo "")
case "$(basename "$COMM")" in
bash|zsh|sh|dash|fish|env) ;; # skip shells — the agent CLI is above them
*) echo -n "pid$PID"; exit 0 ;;
esac
done

# Fallback: no identifiable ancestor
echo -n "pid$$"
Loading