Skip to content

Add multi-session support: multiple agents in the same repo - #8

Open
z3knayr0 wants to merge 1 commit into
PatilShreyas:mainfrom
z3knayr0:feat/multi-session-same-repo
Open

Add multi-session support: multiple agents in the same repo#8
z3knayr0 wants to merge 1 commit into
PatilShreyas:mainfrom
z3knayr0:feat/multi-session-same-repo

Conversation

@z3knayr0

Copy link
Copy Markdown

Problem

Session identity was keyed by project directory: register.sh stored the session ID in a single per-repo pointer (.claude/bridge-session), and reused it for any session registering in that repo. Consequences:

  • Two agent sessions in the same repo collapsed into one logical peer — there was no second inbox to address, so they couldn't communicate.
  • get-session-id.sh's path-prefix fallback returned an arbitrary session when several shared a repo.
  • cleanup.sh (SessionEnd hook) resolved through the shared pointer, so one session exiting could rm -rf a same-repo peer's bridge state.
  • Same-repo peers were indistinguishable in /bridge peers (project name only).

The messaging transport itself was already per-session-ID — only the identity layer assumed one session per project.

Changes

Identity

  • New scripts/get-session-key.sh — stable per-agent-session key: $BRIDGE_SESSION_KEY env override, else the first non-shell ancestor process PID (the agent CLI process, stable per terminal session, distinct across sessions).
  • register.sh — pointer files now live at .claude/bridge-sessions/<session-key>; re-registration reuses only this agent session's bridge session. The legacy .claude/bridge-session file is no longer written (still honored as a read fallback in get-session-id.sh/cleanup.sh for existing installs).
  • get-session-id.sh — resolution order: env → per-session pointer (walks up from cwd, so subdirectories work) → legacy pointer → path scan.
  • cleanup.sh — removes only the calling session's own session dir and pointer file; a same-repo peer's state is never touched.

Labels (same-repo disambiguation)

  • register.sh --as <label> (or $BRIDGE_LABEL); defaults to the current git branch — same-repo sessions are usually on different branches.
  • Manifest gains label; list-peers.sh shows a LABEL column; messages carry metadata.fromLabel; bridge-listen.sh prints FROM_LABEL=; bridge-receive.sh prints Response from <project> [<label>]:; connect-peer.sh shows the peer's label.
  • bridge.md/SKILL.md: peer routing is now label-first, then project name.

Docs & tests

  • README: --as in the commands table, new "Multiple sessions in the same repo" section.
  • Existing tests updated for the pointer layout; new tests/test-same-repo.sh covers the full same-repo flow (distinct registration, labeled peers, query/response round trip, stop-only-own cleanup).
  • Full suite: 183 passed, 0 failed (was 132).

Backwards compatibility

  • Existing single-session-per-project workflows are unchanged (same commands, same message format plus one additive metadata field).
  • Legacy .claude/bridge-session pointers from current installs are still read as a fallback; manifests without label render as empty strings everywhere.

Usage with this PR

# Terminal 1 (same repo)          # Terminal 2 (same repo)
> /bridge start --as backend      > /bridge start --as frontend
> /bridge listen                  > /bridge connect <id>
                                  > /bridge ask "What's the new API shape?"

/bridge peers then shows both sessions with their labels, and the asking side routes by label when project names collide.

Session identity was keyed by project directory (a single
.claude/bridge-session pointer per repo), so a second agent session in
the same repo reused the first session's ID — same-repo peers could not
address each other, and one session's cleanup could destroy the other's
bridge state.

- New get-session-key.sh: stable per-agent-session key (BRIDGE_SESSION_KEY
  env override, else first non-shell ancestor PID)
- register.sh: per-session pointers at .claude/bridge-sessions/<key>;
  optional --as <label> (defaults to git branch) stored in the manifest;
  legacy .claude/bridge-session no longer written
- get-session-id.sh: env -> per-session pointer (walks up from cwd) ->
  legacy pointer -> path scan
- cleanup.sh: removes only the calling session's own dir and pointer
- Labels surfaced everywhere: list-peers LABEL column, message metadata
  fromLabel, FROM_LABEL in bridge-listen, labeled bridge-receive output,
  label in connect-peer output
- heartbeat.sh resolves via get-session-id.sh
- Docs updated (bridge.md, SKILL.md, README); tests updated for the new
  pointer layout; new test-same-repo.sh end-to-end coverage
- Full suite: 183 tests passing (was 132)
@hbmartin

hbmartin commented Aug 6, 2026

Copy link
Copy Markdown

Fwiw, I asked CC to compare this to the similiar PR #5 and here is what it reported:

Both PRs target the same defect in PatilShreyas/claude-code-session-bridge: session identity was keyed by project directory (.claude/bridge-session), so two Claude sessions in one repo collapsed into a single bridge peer, and either one exiting destroyed the other's state. They diverge on what carries session identity.

The core split

PR #5 (fix/per-session-bridge-identity) PR #8 (feat/multi-session-same-repo)
Identity source BRIDGE_SESSION_ID env var, per-process get-session-key.sh — first non-shell ancestor PID
Storage Keeps single shared .claude/bridge-session, demoted to "convenience pointer" New .claude/bridge-sessions/<session-key>, one file per agent session
Resolution order env → shared file → path scan env → per-session pointer (walks up from cwd) → legacy file → path scan
Reuse rule Reuse only if env var set; otherwise always create new Reuse if this key's pointer is valid
Same-repo disambiguation None — peers still show as identical project names --as <label> / $BRIDGE_LABEL, defaults to git branch; plumbed through manifest, messages, list-peers, connect-peer, bridge-receive
Size 171+/74−, 9 files 465+/100−, 19 files (+README)
Tests 144 assertions, all pass 183 assertions, all pass (incl. new test-same-repo.sh)

Both suites are green on my machine, and both merge cleanly.

Why the mechanism choice decides it

PR #5 rests on BRIDGE_SESSION_ID being "per-process, always cor runtime: across independent tool calls, BRIDGE_SESSION_ID andCLAUDE_ENV_FILE are both unset. register.sh writes the var to CLAUDE_ENV_FILE, but the plugin registers only a SessionEnd hook — there's no mechanism
exporting it back into subsequent Bash invocations. So the premhe tests, which pass the var explicitly.

Two consequences, both reproduced:

  1. The original bug survives. cleanup.sh runs as the SessionEndit falls through to the shared pointer — which holds the lastregistered ID:

session A = u9acwk ; session B = rm7vms ; pointer -> rm7vms
--- A's SessionEnd hook fires ---
A session dir still exists? YES
B session dir still exists? NO <-- B destroyed by A exiting

PR #5 guards the pointer file deletion (rm -f only if it's yourSSION_DIR" above it. A exits, B dies, and A leaks.

  1. Session churn. PR Fix: multiple sessions in same repo now get independent bridges #5 removed file-based reuse and rewrote bregister.sh instead of get-session-id.sh for identity. Without the env var, every call mints a new session:

/bridge start -> 9503k2 ; /bridge listen -> tf2x05 ; same? NO

One agent, two bridges, and /bridge listen waits on an inbox nobody addresses.

PR #8's key derives from process ancestry, so it needs no propagation. Same environment, two separate tool calls:

key=pid76859 (the claude process)
key=pid76859 key again=pid76859

Stable, and it resolves identically inside the SessionEnd hook, #5 fails. Isolation holds:

after A exits -> A dir: NO | B dir: YES <-- B survived

Where PR #8 is also imperfect

  • Fallback retracted #4 keeps the old bug. If no pointer matches (stale P, the last-resort projectPath manifest scan still picks anarbitrary same-repo session and rm -rfs it. I confirmed the collateral delete with an unknown key. Narrower blast radius than PR Fix: multiple sessions in same repo now get independent bridges #5, not zero.
  • PID heuristic is environment-dependent. It skips bash|zsh|sh|ere (claude is the direct parent), but any per-command wrapperprocess that isn't in that list would return a fresh PID each call and reintroduce churn.
  • Pointer files accumulate. One pid file per session in the ions/, swept only on clean exit, and .claude/ isn't gitignored.
  • get-session-key.sh and test-same-repo.sh land as 100644 while every sibling is 100755. Harmless — they're invoked via bash — but inconsistent.

Incidental win in PR #8: register.sh builds the manifest with jq -n instead of a heredoc, so project paths or names containing quotes no longer produce
broken JSON. heartbeat.sh also stops duplicating resolution logssion-id.sh.

Reading the two

PR #5 is a tight, minimal community fix — correct diagnosis, rit be per-session, not per-project), but it picked a carrier thatdoesn't survive the runtime, and its tests pass because they inject the var the real system never sets. PR #8 is the fuller treatment: it makes identity
derivable rather than propagated, then addresses the UX half ofuntouched — once two peers share a project name, you need labelsto route between them.

If you're deciding: PR #8 is the one that works, with the caveat that its fallback chain should probably drop the projectPath scan's destructive path — cleanup that can't positively identify its own session should e#5's most salvageable idea is its cleanup.sh ownership check,which PR #8 already incorporates for both pointer files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants