Skip to content

fix: share biometric session across parallel task runners; prevent duplicate daemons#754

Merged
theoephraim merged 1 commit into
mainfrom
fix/turbo-session-and-daemon-race
Jun 5, 2026
Merged

fix: share biometric session across parallel task runners; prevent duplicate daemons#754
theoephraim merged 1 commit into
mainfrom
fix/turbo-session-and-daemon-race

Conversation

@theoephraim
Copy link
Copy Markdown
Member

Summary

Two related fixes uncovered by running varlock through turborepo. When parallel tasks spawned varlock simultaneously, each task fired its own biometric prompt AND each spawned its own daemon (visible as multiple menu bar icons sticking around).

1. TTY session scoping walks past PTY-allocating wrappers

PeerIdentity.swift previously used the peer's immediate controlling TTY. Turborepo (and similar fan-out task runners like nx, pnpm/npm parallel scripts, concurrently, etc.) allocate a fresh PTY per task, so two parallel turbo tasks each resolved to a distinct tty:ttysXXX:… session — meaning each one triggered its own biometric prompt.

Fix: walk up the ancestry chain to the outermost ancestor that still has a TTY and anchor on that. In a plain shell this is a no-op (whole chain shares one TTY until the GUI terminal app, which has no TTY). With turbo et al. in the chain, we walk past their per-task PTYs to the user's terminal TTY. Terminal multiplexers (tmux, screen, zellij) are detected via env vars (TMUX, STY, ZELLIJ) and treated as a session boundary so per-pane scoping is preserved.

2. Daemon flock failure no longer unconditionally bypasses the lock

IPCServer.swift previously detected flock failure and recovered by unlinking the lock file + opening a fresh inode. The comment said this was for "stuck UE state" recovery, but it also defeats the lock entirely during a parallel-spawn race — both daemons end up running.

Fix: triage flock failures by two signals:

  • Lock age < 5s → parallel-spawn race; exit cleanly with {"alreadyRunning": true} so the TS launcher resolves immediately and connects to the winner's socket.
  • Old + socket responds → healthy long-running daemon; exit cleanly.
  • Old + socket unresponsive → kernel-stuck daemon (Secure Enclave wedge, survives even SIGKILL); take over by recreating the lock file inode.

The two-signal check restores the stuck-daemon recovery path (the original intent) while no longer regressing the much more common parallel-spawn case.

daemon-client.ts learns to recognize the alreadyRunning marker on stdout and resolves the spawn promise immediately instead of hanging until the 10s spawn timeout fires.

Test plan

  • Reproduced original bug locally: pnpm turbo run dev in a 2-app workspace prompted twice and left two menu bar icons
  • After fix: same command produces one biometric prompt and one menu bar icon
  • After fix: varlock lock && pnpm turbo run dev still produces one prompt (no regression on locked-state)
  • After fix: tmux with varlock load in two panes still prompts separately (multiplexer env-var detection preserves per-pane scoping)
  • After fix: plain varlock run in a single shell unchanged (TTY-walk is a no-op when whole chain shares one TTY)
  • Worth verifying on CI / older macOS versions if available
  • Worth verifying the "old + unresponsive" stuck-daemon path manually if you can reliably reproduce a stuck daemon

…plicate daemons

Two related fixes uncovered by running varlock through turborepo, where
parallel tasks each triggered their own biometric prompt and spawned
their own daemon (visible as multiple menu bar icons).

1. TTY-based session scoping (PeerIdentity.swift) now walks past
   PTY-allocating wrappers (turbo, nx, pnpm/npm parallel, concurrently,
   etc.) and anchors on the user's outer terminal TTY. Terminal
   multiplexers (tmux, screen, zellij) are detected via env vars and
   treated as a session boundary so per-pane scoping is preserved.

2. Daemon launch (IPCServer.swift, main.swift, daemon-client.ts) no
   longer bypasses its own flock unconditionally — the old "stuck
   recovery" path was the cause of two daemons coming up during a
   parallel-spawn race. Flock failures are now triaged by lock age
   (<5s = race, exit cleanly) and existing-daemon socket
   responsiveness (responsive = healthy, exit; unresponsive + old =
   stuck, take over). The polite-exit case emits {"alreadyRunning":
   true} so the TS launcher resolves immediately instead of waiting
   for the spawn timeout.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

bumpy-frog

The changes in this PR will be included in the next version bump.

patch Patch releases

  • varlock 1.5.0 → 1.5.1

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/varlock@754

commit: 2e4d0ef

@theoephraim theoephraim merged commit b7e30e0 into main Jun 5, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant