Skip to content

feat(coord): cross-surface session presence + serialize worktree creation - #43

Merged
wshallwshall merged 3 commits into
mainfrom
claude/inter-session-communication-3f85ea
Jul 29, 2026
Merged

feat(coord): cross-surface session presence + serialize worktree creation#43
wshallwshall merged 3 commits into
mainfrom
claude/inter-session-communication-3f85ea

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Why

Parallel sessions here were isolated but blind. The SessionStart banner listed worktrees, not live
sessions — so it could not tell you the collision that actually matters: someone editing the shared
primary right now. And no tool on this machine could see a VS Code session at all.

What was established first

The Desktop app's list_sessions MCP tool enumerates an in-memory map of sessions the app itself
spawned
. A session launched by the VS Code extension is never entered into it — not filtered out,
never registered
— so it is invisible there and unaddressable. Verified against a live VS Code session
sharing the default config root, which rules out the per-login explanation.
<config-root>/sessions/<pid>.json is the only registry carrying every surface.

What this adds

scripts/coord/presence.ps1 — a read-only roster of who is actually live in this repo, across
surfaces. Wired into the SessionStart banner, which now calls out a peer in the shared primary and warns
when a peer is on a surface that session messaging cannot reach.

Liveness is fenced, not a pid check: pids get reused and those records outlive their process, so it
compares each process's real start time against the recorded session start. Claude Code ships a
procStart field for exactly this, but here it serialises as absent and the guard passes
unconditionally — a bare pid check reports a recycled pid as a live session.

scripts/coord/lock.ps1 + new.ps1git worktree add -b <name> <base> writes .git/config, so
concurrent adds race .git/config.lock, leaving orphaned branches behind. That call is now serialized
through the same atomic exclusive-create claim.ps1 uses. It retries and never steals: with no
reliable liveness signal on this host, breaking a lock you cannot prove is abandoned re-opens the race
it exists to close, so on timeout it fails loudly and names the holder.

Verification

19 new tests, all driving the real scripts as subprocesses against throwaway repos and fixture
config roots — never a Python copy of the rules.

Two mutation checks, because a green test that would pass anyway proves nothing:

Mutation Expected Result
Disable the start-time fence in presence.ps1 the pid-reuse test fails, others hold ✅ fails; dead_pid still passes, so it is surgical
CreateNewCreate in lock.ps1 both concurrency tests fail ✅ both red

The lock's headline test launches eight real concurrent processes and asserts exactly one wins and
the losers fail rather than silently proceed — eight because a read-modify-write in this codebase was
measured silently losing 4 of 8 concurrent PowerShell writes.

The SessionStart tests pin the fail-safe contract: a missing, throwing, or junk-emitting presence.ps1
costs the live-session list and nothing else, because whatever that hook prints is the chat's starting
context. One test asserts the positive arm too — otherwise a no-op integration would satisfy every
absence assertion.

ruff format, ruff check, mypy clean.

Not included

A live cross-session defect in user-global ~/.claude/settings.json (three hooks share one
$env:TEMP\claude-waiting.flag, so any session's prompt-submit clears a flag another live session set)
is out of scope for this repo and was blocked by the permission classifier. The tested fix is in the
session notes for the owner to apply by hand.

🤖 Generated with Claude Code

The SessionStart banner listed WORKTREES, not live sessions -- so it could not tell
you that someone is editing the shared primary right now, which is the collision that
actually matters. And no existing tool could see a VS Code session at all.

Why the Desktop app's own session tooling cannot do this: its list_sessions enumerates
an in-memory map of sessions THE APP ITSELF SPAWNED. A VS Code session is never entered
into it -- not filtered out, never registered -- so it is invisible and unaddressable.
Verified 2026-07-29 against a live VS Code session sharing the DEFAULT config root, so
this is not a per-login split. <config-root>/sessions/<pid>.json is the only registry
carrying every surface; presence.ps1 reads that, discovering config roots dynamically
because several logins can coexist.

Liveness is a fence, not a pid check. Pids get reused and these records outlive their
process, so presence.ps1 computes process start time itself and requires it to be
consistent with the recorded session start. Claude Code ships a procStart field for
exactly this, but on this host it serialises as absent and its guard passes
unconditionally -- so a bare pid check would report a recycled pid as a live session.

Read-only throughout: a roster, not a channel. It never writes a registry file and
never contacts another session.

Tests drive the real scripts as subprocesses against throwaway repos and fixture config
roots, never a Python copy of the rules. The fence test was verified by mutation: with
the start-time check disabled it fails while the dead-pid test still passes, so it is
pinning the fence rather than the script merely running. The session-context tests pin
the fail-safe contract -- a missing, throwing, or junk-emitting presence.ps1 costs the
live-session list and nothing else, because whatever that hook prints IS the chat's
starting context.
…n layer

`git worktree add -b <name> <base>` writes .git/config (the branch's upstream), so two
sessions creating worktrees at once race .git/config.lock. On Windows that surfaces as
"could not lock config file .git/config: File exists" / "unable to write upstream branch
configuration", leaving ORPHANED branches behind and callers that never run. Several
worktrees already share this .git, so it is a live hazard rather than a theoretical one,
and nothing serialized it.

scripts/coord/lock.ps1 is the primitive: the same atomic exclusive-create as claim.ps1
and alloc.ps1, because a read-modify-write was measured silently losing 4 of 8 concurrent
PowerShell writes. It is deliberately NOT claim.ps1 -- a claim is a long-lived advisory
note about WORK, this is a short mutex around one OPERATION, and that is why this retries
where claims do not.

It RETRIES and NEVER STEALS. There is no reliable liveness signal on this host (the
session registry has no heartbeat, and its shipped pid+procStart guard fails OPEN toward
"still alive"), so breaking a lock we cannot prove abandoned would re-open the race the
lock exists to close. On timeout it fails loudly, naming the holder and the manual
override. A wedged lock you can see beats a silent double-write you cannot.

Tests launch eight real concurrent processes and assert exactly one wins, and that the
losers FAIL rather than silently proceed -- verified by mutation: swapping CreateNew for
Create turns both concurrency tests red while the others stay green, so they pin the
exclusion rather than the script merely running. One test takes the lock from the primary
and contends from a worktree, since a lock only helps if every worktree resolves to the
same file.

docs/WORKTREES.md now covers presence.ps1 and this lock, including why a DEAD verdict must
never by itself authorise a destructive action.
@wshallwshall
wshallwshall enabled auto-merge (squash) July 29, 2026 15:28
@wshallwshall
wshallwshall merged commit 8862d92 into main Jul 29, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the claude/inter-session-communication-3f85ea branch July 29, 2026 15:50
wshallwshall added a commit that referenced this pull request Jul 29, 2026
messagefoundry-webconsole 0.2.15 published to PyPI 2026-07-29 by the first
webconsole-v* release, over Trusted Publishing (OIDC, no API token). Verified:
pypi.org returns 200, one file, 0.2.15; the run's release-webconsole job
succeeded with the publish step RUN (not skipped) while release and
release-harness correctly skipped on the tag prefix.

This is the half that actually closes the cell. The earlier commit corrected our
install instructions, which removed OUR CONTRIBUTION to the dependency-confusion
risk but could not remove the risk: the name stayed free for anyone to register,
and a squatted sdist executes its build backend during `pip install`, before any
engine process exists. Claiming the name is what forecloses the substitution.

Docs flipped back to real index installs (README, INSTALL-GUIDE, SERVICE,
USER-GUIDE, MENTAL-MODEL, packaging README), and api/app.py's serve_ui
RuntimeError now prints a command that works.

The guard is UPDATED, not deleted:

- _UNPUBLISHED_DISTRIBUTIONS is now empty, which is the goal state rather than a
  disabled check. To stop it rotting back to vacuous,
  test_a_new_distribution_must_be_classified requires every distribution under
  packaging/ to be classified published or unpublished, so a NEW packaging/<name>/
  that is neither fails the build. That is precisely the window this exposure
  lived in: a distribution existed, shipped docs named it, nobody had claimed it.

- The guard-the-guard was exercising the path-vs-index discrimination using the
  REAL name, so three of its six cases inverted the moment the name was claimed
  — it would have failed for being right. Repointed at a synthetic never-to-exist
  name, with an assertion that the probe name stays fictional, plus new cases
  asserting a PUBLISHED name is not flagged (claiming a name is what makes a
  bare-name instruction safe, so the detector must say so).

Two mutations red: leave a packaged distribution unclassified; reinstate the
non-existent [webconsole] extra in shipped code.

9579 passed. The two failures are environmental and reproduce without this
change: no .venv in this worktree (stale 0.3.0 install vs 0.3.2 source), and
main's #43 moved the gate source while the installed user-scope snapshot is
stale (needs install-gate.ps1 re-run).
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.

1 participant