Conversation
|
|
Speaking as Kun's firstmate: first stamp vs tip Closes verification: GitHub closing ref + body HEAD contract-class: restore — tip still cannot resolve harness identity across the Cygwin/Windows boundary ( VISION (compressed): One captain/interface — aligns (restores usable session ownership, not a new captain surface). Authority explicit — aligns (fail-closed when pid unpublished/non-harness; nested-inherit limitation documented). Scripts/judgment — aligns (deterministic ps/pid bridge). Restart non-event — aligns (tagged liveness/stale recovery). Delegation spine — n/a. Fleet outlives vendor — aligns (platform-shaped adapter for Cygwin boundary). Scope — aligns (lock mechanics, not workshop). Needed: green Lint 1 (or tip envelope landing) before merge consideration; Greptile P1 already documented as accepted Windows limitation. Not auto-merge eligible this pass. Firstmate flag no. |
Every session start on Cygwin (Git for Windows) refused the fleet lock and dropped to read-only with "cannot locate harness process in ancestry", so spawning, steering, merging, the wake-queue drain, and supervision repair were skipped on every start. Two independent causes, both on the identity path. Cygwin's ps has no -o option at all and fails the whole invocation with "unknown option -- o", so the ancestry walk aborted on its first hop. The walk now reads comm, args, and ppid through accessors that fall back to Cygwin's fixed ps columns, leaving the procps/BSD path unchanged. That alone does not resolve the session: the parent link from a shell the harness spawns does not cross the Cygwin boundary, and Cygwin reports that shell's PPID as 1, so no walk can reach a harness that is a native Windows process. Identity is instead taken from the session pid the harness publishes and confirmed against the Windows process table before it is used - the pid must still be live and its executable must independently identify a verified harness - so an absent, stale, or non-harness value is discarded rather than bound. Walking the real Windows parent chain was implemented and then removed as unsafe. MSYS emulates exec by spawning a fresh Windows process and exiting the old one, so intermediate shells vanish and a child's recorded parent is routinely a pid that no longer exists; Windows never reparents an orphan, so that dangling id stays and can be reissued to an unrelated process. Following it can bind a home's lock to the wrong process, which is the failure this file exists to prevent. A harness that publishes nothing stays unresolved, which leaves the session read-only exactly as before. Windows pids are tagged rather than stored bare. They are a different namespace: kill -0 reports a live Windows process as dead, and the number can collide with an unrelated live Cygwin pid. The tag makes the value non-numeric, so a consumer that treats it as a local pid - including a future kill - refuses it instead of acting on the wrong process. fm-sessionstart-nudge.sh carried a private second copy of the ownership walk and so stayed wrong after the owner was fixed, nudging a session that already held the lock. It now asks the owning function. Verified on Windows 11 (Git Bash, Cygwin ps 3.4.10): the lock is acquired, reports its holder, is idempotent, and refuses a bogus, dead, or non-harness published pid. Regressions cover both platform departures behind a fake process table, so they run on Linux and macOS CI too. The pre-existing e2e failure in this suite on Windows is unchanged from main; it cannot exec its symlinked fixture. Refs kunchenguid#3396 Claude-Session: https://claude.ai/code/session_01F9T29YDqYSkQeDTC2Nfi7h (cherry picked from commit 8b281b7)
Delegating the nudge to fm_session_lock_owned_by_self changed the question it asks. The nudge asks whether a process in this ancestry took the lock; the ownership predicate additionally requires a verified harness in that ancestry, so a session whose lock was written by a plain shell started being nudged to run session start again. tests/fm-sessionstart-nudge.test.sh pins the looser contract deliberately. The walk stays local, now reading ppid through the portable accessor so it also survives a ps with no -o option, and defers to the ownership predicate only for a Windows-tagged holder, which is not in this process table at all. (cherry picked from commit 0baf64f)
…the lock The Windows identity added in this branch introduced a second shape into state/.lock. Six gates read that field, and each one answered "is this value usable" with its own inline numeric test, so every one of them read a valid Windows holder as malformed. The visible cost was concentrated in startup completion: the record was never written, and the clear/compact check that consumes it could never match, so every clear or compact on Windows repeated the full startup sequence. The deferred network sweeps reported ownership as changed when it had not, and the Stop auto-arm treated a dead Windows session as an unreadable lock rather than a recoverable one. fm-lease.sh was the outlier: rather than refusing a value it could not use, `tr -cd '0-9'` reduced the tag to its digits and produced a number naming an unrelated process in the local table. It happened to fail closed downstream, but deriving a wrong-namespace pid is precisely what the tag exists to prevent, so it now takes the value whole and requires a local pid. fm_session_pid_valid is now the single owner of that question and every gate delegates to it, so a third identity shape cannot split them again. Verified against the shipped bytes of each gate with a tagged lock: startup completion now records and is recognized (main's gate reruns the full startup on the same input), both network-ownership gates authorize their sweeps, and a tagged lock yields no lease holder pid instead of 7204. (cherry picked from commit 9256182)
… every lock reader Accept win:<digits> only where fm_win_boundary_applies, with an all-digit remainder, so a Linux/macOS reader of a win:N lock stays inert as before. The sessionstart nudge routes its tagged case through fm_win_untag_pid. fm_session_lock_foreign_owner_live and fm_session_lock_inspect now validate through fm_session_pid_valid and resolve a tagged holder through fm_harness_pid_alive, so a live win:N holder reads as a live foreign owner and as held; unverifiable tagged holders stay unknown.
…in nudge and scripts docs
… tagged holder A Windows session can now hold the session lock as win:<pid>, but the task lease still accepted only a local pid: the claim recorded the short-lived fm-lease.sh process instead, and fm_lease_live rejected the tagged lock, so the lease was never live and the other supervision actor could clear it and act on the same task. The lease now records a live tagged lock holder whole and treats it as live while it is still the lock holder and a verified harness in the Windows process table. The session-lock library is loaded only for a win:-shaped value, so POSIX lease verdicts are unchanged. fm_session_lock_inspect left an exited tagged holder unknown, because fm_win_command could not tell an absent pid from an unreadable table. It now returns 1 only when a readable table proves the pid absent and 2 when the table cannot be read, so inspection matches the local-pid contract: absent is stale, a live non-harness is unknown with no live harness, and an unreadable table stays unknown.
…ithout their fixes
724da26 to
926681f
Compare
Intent
Let a Firstmate session on Windows/Git Bash acquire its session lock instead of always dropping to read-only, without changing POSIX behavior.
This is the first small piece split out of #4803. It carries the session-identity work from #3553 by @lmktechnology, rebased onto current main with his authorship preserved, plus follow-up commits that fix review findings.
Walking the real Windows parent chain was rejected as unsafe: MSYS exec emulation leaves dangling parent ids that Windows can reissue to unrelated processes. A harness that publishes no session pid stays read-only.
It does not depend on the #506 platform seam, and either can adapt to the other.
What Changed
bin/fm-session-lock-lib.shgains a Windows/Git Bash identity path, gated onfm_win_boundary_applies: a Cygwinpsfallback (fm_ps_ppid) for whenps -ois unsupported, and a session identity taken from the harness-published session pid, confirmed against the Windows process table and recorded as a taggedwin:<pid>. The real Windows parent chain is never walked, and a harness that publishes no pid stays read-only.bin/fm-lock.shkeepsfm_session_lock_anchor_pidand adds a Windows-specific error message. POSIX/Linux/macOS behavior is unchanged: the tagged shape is accepted only where the Windows boundary applies and the part afterwin:is all digits.fm-bootstrap,fm-claude-stop-autoarm,fm-session-start,fm-sessionstart-run,fm-startup-network,fm-lock) accepts the tagged identity throughfm_session_pid_valid.fm_session_lock_foreign_owner_liveandfm_session_lock_inspectnow treat a live tagged holder as live.fm_session_lock_inspectalso treats an exited tagged holder as stale, becausefm_win_commandnow returns a different code for an absent pid than for an unreadable process table. The sessionstart nudge keeps its own ancestry question and hands a tagged holder tofm_session_lock_owned_by_self.fm-lease.shandfm-supervision-host.shpass the lock value through whole instead of stripping it to digits, andfm_lease_livechecks awin:holder against the Windows process table. The session-lock library is loaded only for awin:-shaped value. Docs (docs/scripts.md,docs/sessionstart-nudge.md) describe the tagged identity.tests/fm-session-lock-ancestry.test.shandtests/fm-supervision-host.test.shgain Cygwin/Windows and tagged lease-holder regression tests.Fixes #3396. Refs #4535, #4539 (their remaining numeric-only readers are out of scope here).
Known limitations
CLAUDE_PID), confirmed live and harness-shaped in the Windows process table. A separate harness started from inside a Claude window inherits that variable, so it can be treated as the same session and pass the ownership check. On Linux/macOS the same nested harness would not own the lock. Windows exposes no reliable parent chain to tell them apart, so this is accepted as a limitation of the published-pid approach.MSYS=winsymlinks:nativestrict); without it the claim lock never succeeds. That is the existing issue Windows: fm_lock_try_create can never succeed without symlink privilege (ln -s silently degrades to a copy) #3267 and is not changed here.Risk Assessment
✅ Low: The last fix round changes only tests. The two tests now fail without their fixes: the caller-supplied win:7300 case catches removal of the win:* arm, and the host test asserts holder=win:7204 when driven through the real host. The production lease and host code from earlier rounds is unchanged, and Windows behavior stays gated on win:-shaped values.
Testing
I drove every scenario live on this real Windows 11 Git Bash host. The scripts ran from the gate worktree against disposable lab homes made with fm-lab-home.sh; each lab was removed afterwards, and the worktree is clean. Identities came from real processes: this session's live claude.exe (CLAUDE_PID), another live claude.exe until it exited on its own partway through, and a claude.exe stand-in that this test started and stopped itself. Everything passed. Covered: acquire, refusal, status, inbox ready, stale reclaim, malformed-value rejection, read-only fallbacks, the task lease following the tagged holder, and POSIX inertness under a faked Linux uname. A base-vs-HEAD control shows the fix is real. Two environmental issues, neither a regression from this change. First, lease claims hang when the lab sits under the Git Bash /tmp mount (symlink readlink path mismatch, the same family as upstream #3267), so the lease scenarios used a worktree-local lab; a numeric-lock control behaves the same way. Second, the ancestry suite passes all 15 tests for this change on Git Bash but then stops at a fixture inherited from main that is Linux-only (
ps -o ppid=). Base fails earlier on this host, and the suite was already reported to pass in full in a Linux container. There is no UI surface, so the evidence is CLI transcripts.Evidence: Live lock driver transcript (acquire, refusal, status, stale reclaim, malformed, read-only)
Source: Live lock driver transcript (acquire, refusal, status, stale reclaim, malformed, read-only)
Evidence: Live lease + POSIX-inert transcript
Source: Live lease + POSIX-inert transcript
Evidence: Base vs HEAD control with a live tagged holder
Source: Base vs HEAD control with a live tagged holder
-- BASE e31bc6e lock: stale (pid win:28020 dead or not a harness) error: cannot locate harness process in ancestry foreign_live=no -- HEAD 926681f lock: held by live harness pid win:28020 error: another live firstmate session holds the lock (pid win:28020); operate read-only until resolved foreign_live=yes pid=win:28020 "lock":{"state":"held","pid":null,"live_harness":true} -- stand-in stopped; HEAD now lock: stale (pid win:28020 dead or not a harness) lock acquired: harness pid win:3268Evidence: Driver scripts
Source: Driver scripts
Evidence: Driver scripts (lease/POSIX)
Source: Driver scripts (lease/POSIX)
Evidence: Ancestry suite on Git Bash (HEAD)
Source: Ancestry suite on Git Bash (HEAD)
Evidence: Ancestry suite on Git Bash (base control)
Source: Ancestry suite on Git Bash (base control)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed (3) ✅
bin/fm-lease.sh:132- Simplification:fm-lease.sh claimnow also accepts a Windows-tagged value through the caller-supplied FM_LEASE_HOLDER_PID (line 132), in addition to the state/.lock fallback (line 138). The approved follow-up only asks that the task lease "record and honor a live tagged lock holder". The lock fallback alone does that: when a caller passes a win: value and this branch is absent, the script falls through to the same lock holder. The callers that pass FM_LEASE_HOLDER_PID are the Pi branch extensions, and those are explicitly deferred to fix: numeric-only session-lock readers still reject a Windows-tagged win:<pid> lock #4539. Suggested remedy: drop thewin:*arm at line 132 and keep only the lock-fallback arm at line 138. This needs author confirmation because it narrows an accepted input.🔧 Fix applied.
1 warning still open:
bin/fm-supervision-host.sh:959- fa3cd63 was meant to make the task lease record and honor a live tagged lock holder. It fixed only the state/.lock fallback in fm-lease.sh claim and missed the main caller that supplies FM_LEASE_HOLDER_PID: the Claude-home supervision host. That caller still setsFM_LEASE_HOLDER_PID=$(sed -n '1p' "$STATE/.lock" | tr -cd '0-9'). Failing sequence on Windows/Git Bash: the lock holdswin:7204. host_still_owner passes, because fm_session_lock_owned_by_self accepts the tagged identity (bin/fm-supervision-host.sh:456). The engine subshell then exports FM_LEASE_HOLDER_PID=7204. Since a7ada0b, fm-lease.sh:131 accepts only a bare local number, so it takes 7204 and the lease recordsbranch\t7204. In fm_lease_live (bin/fm-lease-lib.sh:207-223), 7204 is not win:-shaped, so the check falls to kill -0 7204. That either fails or hits an unrelated Cygwin pid, and thenlock_pid=win:7204fails the numeric case. The branch actor's lease therefore always reads stale, and the main actor can clear it and act on the same task. That is the failure the approved follow-up says is fixed. It also contradicts fm-lease.sh's own comment that values must never be reduced to their digits. Neither fix round introduced this: fa3cd63 left the sibling behind, and a7ada0b's narrowing neither causes nor fixes it. Minimal fix: export the whole first line withouttr -cd '0-9'. A tagged value then fails fm-lease.sh:131, falls through to the lock fallback at fm-lease.sh:133-137, and recordswin:7204; a numeric POSIX lock behaves as before. Only one site needs the fix: no other caller derives FM_LEASE_HOLDER_PID from the lock, and the Pi extensions are deferred to fix: numeric-only session-lock readers still reject a Windows-tagged win:<pid> lock #4539.🔧 Fix applied.
1 warning still open:
tests/fm-session-lock-ancestry.test.sh:694- The round-3 regression test (added with 8e30f5c) cannot fail without that round's fix. The lock in this test already holds win:7204. If the restoredwin:*arm for FM_LEASE_HOLDER_PID (bin/fm-lease.sh:132) is deleted, the value is cleared and the claim falls through to the state/.lock fallback (bin/fm-lease.sh:137-141). That fallback records the same win:7204, so both assertions (the recorded holder and the main actor's exit 6) still pass. The defect R2-1 actually reported was the host's digit-stripping at bin/fm-supervision-host.sh:959, and the test never exercises it. Reverting that line to| tr -cd '0-9'would also leave the test green. Make the test fail before the fix. One option is to drive the host's lease export, or a minimal extraction of it, against a win:7204 lock and assert the recorded holder is win:7204 and not 7204. Another is to give the caller-supplied arm a case the fallback cannot produce, for example FM_LEASE_HOLDER_PID=win:7204 while the lock names a different live tagged holder, and assert which value is recorded. (The batched env lines at 694/697 also lost their backslash continuations and are now long single lines with runs of spaces. They still work but no longer match the surrounding style.)🔧 Fix applied.
✅ Re-checked - no issues remain.
bin/fm-wake-lib.sh:584- On Git Bash, a lab home under the /tmp mount makes fm-lease.sh claim spin until timeout. The symlink owner path read back by readlink does not match the path that was written. This is the pre-existing Windows: fm_lock_try_create can never succeed without symlink privilege (ln -s silently degrades to a copy) #3267-family symlink-lock issue: it is independent of the lock value and also happens with a numeric lock. A worktree-local TMPDIR avoids it. It is not a regression from this change.tests/fm-session-lock-ancestry.test.sh:821- tests/fm-session-lock-ancestry.test.sh cannot finish on Windows Git Bash. After all 15 tests for this change pass, it stops at a fixture inherited from main (fix(bin): correct session lock and attached watcher supervision #1545/fix(bin): preserve Claude lock ownership after helper recycling #4894) that relies on Linuxps -o ppid=. Base e31bc6e fails earlier on the same host. The suite has been reported to pass in full in a Linux container.bash live-lock-driver.sh 6448 999996(evidence dir): fm-lock.sh acquire/status/re-acquire on a free lab home, owned_by_self/foreign_owner_live, refusal under a live foreign win: holder, inbox ready lock fields, stale+reclaim of a dead win: holder, malformed win: values, no-published-pid and non-harness CLAUDE_PID read-only paths, lease claim/refusalbash live-lease-posix-driver.sh 6448(evidence dir, lab under a worktree-local TMPDIR): lease claim with the host'ssed -n 1p state/.lockexport records win:<pid> and is live, main actor refused with exit 6, lease turns stale when the holder is dead; fake uname=Linux makes a win:N lock inert in fm-lock.sh status, fm_session_pid_valid, foreign_owner_live and fm-claude-stop-autoarm.sh (lock untouched)Before/after control: base e31bc6ebin/(git archive) vs HEAD against a live tagged stand-in holder (renamed ping.exe started and stopped by this test), covering fm-lock.sh status/acquire, foreign_owner_live and inbox ready, then reclaim after the stand-in exitsMSYS=winsymlinks:nativestrict bash tests/fm-session-lock-ancestry.test.shon Git Bash for HEAD and for base e31bc6e as a control✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.