feat(coord): cross-surface session presence + serialize worktree creation - #43
Merged
Merged
Conversation
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
enabled auto-merge (squash)
July 29, 2026 15:28
This was referenced Jul 29, 2026
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_sessionsMCP tool enumerates an in-memory map of sessions the app itselfspawned. 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>.jsonis 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, acrosssurfaces. 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
procStartfield for exactly this, but here it serialises as absent and the guard passesunconditionally — a bare pid check reports a recycled pid as a live session.
scripts/coord/lock.ps1+new.ps1—git worktree add -b <name> <base>writes.git/config, soconcurrent adds race
.git/config.lock, leaving orphaned branches behind. That call is now serializedthrough the same atomic exclusive-create
claim.ps1uses. It retries and never steals: with noreliable 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:
presence.ps1dead_pidstill passes, so it is surgicalCreateNew→Createinlock.ps1The 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.ps1costs 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,mypyclean.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