Follow-up to #221 / #223. This is a removal gate, not a cleanup ticket: the work is
the proof, and the deletion is the last step of it. Nothing here should be actioned
because the code "looks like dead migration cruft".
Acceptance invariant
After migration, supervisor authority is keyed solely to the tmux socket namespace.
Checkout location must have zero effect on whether two supervisors contend.
Why the bridge exists (do not re-derive this from the comments)
The bridge exists solely to prevent a mixed-version deployment from creating the exact
two-supervisor window #221 fixed. Removing it early is a regression in authority
containment, not cleanup debt.
#223 re-keyed the guard from brain_root()/.supervisor.lock (checkout-relative) to
<socket>.supervisor.lock (host-global). A pre-bridge binary knows only the first
key. So for as long as one can be started, a post-bridge supervisor must hold both
locks, or the two versions guard different inodes and both start.
The cost, which is the thing that gets removed here: while the bridge holds, two
supervisors on different sockets in the same checkout also contend, because both
must take that checkout's legacy lock. The migration-era namespace is the pair
(socket, checkout).
The gate — every item must be proved before touching _BRIDGE_HOLDS_LEGACY_LOCK
Audit run 2026-08-19, immediately after #223 merged as 663b378d. Two items fail
today. Re-run all of it at removal time; do not trust this table's age.
G1 — Enumerate every checkout on the host that can plausibly start bin/px-brain
find /home/pi -maxdepth 3 -name px-brain -type f
| Checkout |
HEAD |
Branch |
Contains 663b378d |
legacy_supervisor_lock_path refs |
/home/pi/picar-x-hacking |
663b378d |
master |
YES |
3 |
/home/pi/px-brain-wt |
efe22d42 |
e2e/brain-holder-verify |
NO |
0 |
❌ FAILS. /home/pi/px-brain-wt is a runnable pre-bridge checkout. Its
brain_daemon.py contains no reference to the socket lock at all, so a supervisor
started from it guards only state/brain/.supervisor.lock under that worktree. This is
precisely the binary the bridge exists to lose to.
Resolution is either: rebase/merge that worktree past 663b378d, or delete it if the
e2e/brain-holder-verify work is finished. Prove it per checkout, not per branch —
a merged branch whose worktree still sits on an old commit is still a runnable
pre-bridge binary.
G2 — Record each checkout's commit and prove it contains the bridge
for d in $(find /home/pi -maxdepth 3 -name px-brain -type f -printf '%h\n' | xargs -n1 dirname); do
git -C "$d" merge-base --is-ancestor 663b378d HEAD && echo "$d OK" || echo "$d PRE-BRIDGE"
done
Ancestry, not a grep for the function name — a checkout can carry the symbol from a
partial cherry-pick without carrying the dual acquire.
G3 — Inspect the active unit's WorkingDirectory and executable path
systemctl cat px-brain.service | grep -E '^(WorkingDirectory|ExecStart)='
WorkingDirectory=/home/pi/picar-x-hacking
ExecStart=/home/pi/picar-x-hacking/bin/px-brain
✅ PASSES. One unit, pointing at the bridge-aware checkout.
G4 — Confirm no unit, cron, wrapper, shell script or other launcher points at a pre-bridge checkout
ExecStart is not the only way in. Sweep at minimum:
systemctl list-units --all | grep -i brain
grep -rl 'px-brain' /etc/systemd/system /etc/cron* /home/pi/*/bin 2>/dev/null
crontab -l; sudo crontab -l
ssh M5.local 'crontab -l' # M5 pushes to this host; check it cannot invoke px-brain
Not yet swept — the M5 cron leg in particular has not been checked.
G5 — Confirm the running production supervisor is bridge-aware
Not "the unit points at a bridge-aware checkout" (G3) — the live process. A
bridge-aware supervisor holds two locks; a pre-bridge one holds only the legacy lock.
That is directly observable:
ls -la /tmp/tmux-1000/*.supervisor.lock # socket lock present?
sudo lsof "$(git rev-parse --show-toplevel)/state/brain/.supervisor.lock"
Observed 2026-08-19:
socket lock: ABSENT (/tmp/tmux-1000/ holds only the px-mind socket itself)
legacy lock: python3 PID 740 pi 3uW .../state/brain/.supervisor.lock
PID 740 started 12:16:19 AEST = 02:16 UTC; #223 merged 06:18 UTC
❌ FAILS. The running supervisor predates the merge and holds only the legacy lock.
It becomes bridge-aware on systemctl restart px-brain and not before. Until then the
host is in the mixed state the bridge is written for — which is the bridge working, not
a problem to fix by deleting it.
G6 — Only then, remove
In one commit:
_BRIDGE_HOLDS_LEGACY_LOCK and the if not ... branch in acquire_supervisor_lock()
legacy_supervisor_lock_path()
- the second acquire, its failure path (the
_drop_lock(fd)-then-refuse branch), and
_legacy_fd throughout, including in release_supervisor_lock()
TestMigrationBridge in tests/test_supervisor_lock_scope.py
TestContention::test_after_removal_the_socket_alone_is_the_namespace (case 4 —
it exists to describe this end state and is redundant once reached)
- the migration paragraphs in
CLAUDE.md and acceptance criterion 2 of
docs/specs/2026-08-19-supervisor-lock-scope-migration-design.md
Post-removal test expectations
TestContention is written so that removal shows up as a specific, single flip —
if anything else moves, the removal broke containment:
| Case |
Test |
Before |
After |
| 1 |
test_two_checkouts_on_one_socket_cannot_both_acquire |
contend |
contend (unchanged — this is #221's fix) |
| 2 |
test_different_sockets_and_different_state_roots_coexist |
coexist |
coexist (unchanged) |
| 3 |
test_different_sockets_in_one_checkout_contend_during_migration |
contend |
flips to coexist — rewrite the assertion and drop "during migration" from the name |
| 4 |
test_after_removal_the_socket_alone_is_the_namespace |
passes under simulation |
deleted |
Case 1 staying red-for-a-second-supervisor is the invariant. A removal that also
relaxes case 1 has re-opened #221.
Notes for whoever picks this up
_BRIDGE_HOLDS_LEGACY_LOCK is deliberately not env-readable. Do not add an
override to "test removal in production" — flipping it live is exactly the two-
supervisor window. Case 4 simulates the end state in-process; that is the supported
way to see it.
- Re-run the whole audit at removal time. G1 and G5 are both time-sensitive: a new
worktree or an un-restarted daemon re-opens them silently.
- Per
CLAUDE.md, do not run the full suite on SPARK — CI is the gate.
Refs #221, #223.
Follow-up to #221 / #223. This is a removal gate, not a cleanup ticket: the work is
the proof, and the deletion is the last step of it. Nothing here should be actioned
because the code "looks like dead migration cruft".
Acceptance invariant
Why the bridge exists (do not re-derive this from the comments)
#223 re-keyed the guard from
brain_root()/.supervisor.lock(checkout-relative) to<socket>.supervisor.lock(host-global). A pre-bridge binary knows only the firstkey. So for as long as one can be started, a post-bridge supervisor must hold both
locks, or the two versions guard different inodes and both start.
The cost, which is the thing that gets removed here: while the bridge holds, two
supervisors on different sockets in the same checkout also contend, because both
must take that checkout's legacy lock. The migration-era namespace is the pair
(socket, checkout).The gate — every item must be proved before touching
_BRIDGE_HOLDS_LEGACY_LOCKAudit run 2026-08-19, immediately after #223 merged as
663b378d. Two items failtoday. Re-run all of it at removal time; do not trust this table's age.
G1 — Enumerate every checkout on the host that can plausibly start
bin/px-brain663b378dlegacy_supervisor_lock_pathrefs/home/pi/picar-x-hacking663b378dmaster/home/pi/px-brain-wtefe22d42e2e/brain-holder-verify❌ FAILS.
/home/pi/px-brain-wtis a runnable pre-bridge checkout. Itsbrain_daemon.pycontains no reference to the socket lock at all, so a supervisorstarted from it guards only
state/brain/.supervisor.lockunder that worktree. This isprecisely the binary the bridge exists to lose to.
Resolution is either: rebase/merge that worktree past
663b378d, or delete it if thee2e/brain-holder-verifywork is finished. Prove it per checkout, not per branch —a merged branch whose worktree still sits on an old commit is still a runnable
pre-bridge binary.
G2 — Record each checkout's commit and prove it contains the bridge
Ancestry, not a
grepfor the function name — a checkout can carry the symbol from apartial cherry-pick without carrying the dual acquire.
G3 — Inspect the active unit's
WorkingDirectoryand executable path✅ PASSES. One unit, pointing at the bridge-aware checkout.
G4 — Confirm no unit, cron, wrapper, shell script or other launcher points at a pre-bridge checkout
ExecStartis not the only way in. Sweep at minimum:Not yet swept — the M5 cron leg in particular has not been checked.
G5 — Confirm the running production supervisor is bridge-aware
Not "the unit points at a bridge-aware checkout" (G3) — the live process. A
bridge-aware supervisor holds two locks; a pre-bridge one holds only the legacy lock.
That is directly observable:
Observed 2026-08-19:
❌ FAILS. The running supervisor predates the merge and holds only the legacy lock.
It becomes bridge-aware on
systemctl restart px-brainand not before. Until then thehost is in the mixed state the bridge is written for — which is the bridge working, not
a problem to fix by deleting it.
G6 — Only then, remove
In one commit:
_BRIDGE_HOLDS_LEGACY_LOCKand theif not ...branch inacquire_supervisor_lock()legacy_supervisor_lock_path()_drop_lock(fd)-then-refuse branch), and_legacy_fdthroughout, including inrelease_supervisor_lock()TestMigrationBridgeintests/test_supervisor_lock_scope.pyTestContention::test_after_removal_the_socket_alone_is_the_namespace(case 4 —it exists to describe this end state and is redundant once reached)
CLAUDE.mdand acceptance criterion 2 ofdocs/specs/2026-08-19-supervisor-lock-scope-migration-design.mdPost-removal test expectations
TestContentionis written so that removal shows up as a specific, single flip —if anything else moves, the removal broke containment:
test_two_checkouts_on_one_socket_cannot_both_acquiretest_different_sockets_and_different_state_roots_coexisttest_different_sockets_in_one_checkout_contend_during_migrationtest_after_removal_the_socket_alone_is_the_namespaceCase 1 staying red-for-a-second-supervisor is the invariant. A removal that also
relaxes case 1 has re-opened #221.
Notes for whoever picks this up
_BRIDGE_HOLDS_LEGACY_LOCKis deliberately not env-readable. Do not add anoverride to "test removal in production" — flipping it live is exactly the two-
supervisor window. Case 4 simulates the end state in-process; that is the supported
way to see it.
worktree or an un-restarted daemon re-opens them silently.
CLAUDE.md, do not run the full suite on SPARK — CI is the gate.Refs #221, #223.