Repository navigation
fix: exactly-once delivery across processes sharing a data directory (#14) - #32
Conversation
…ems (#9) Resolve the data directory as explicit option, BUGSEE_DATA_DIR, platform default, then temp dir, taking the first that is actually writable. If none is, or the disk fails mid-run (full, read-only, removed), launch still succeeds and handled errors / manual reports are assembled without capture context and delivered from a bounded in-memory backlog. Native crash handler and panic snapshots are skipped in memory mode. Adds Bugsee::storage_status() so a host can check crash recovery is available and whether the dir may be ephemeral. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- memory backlog: entries stay queued while in flight so the cap is real; cache the presigned endpoint (no duplicate issue on a retried PUT); honour the flush deadline; clear the backlog in fork children - a requested data_dir / BUGSEE_DATA_DIR is final: unusable means memory mode (explicit_rejected), never another disk location - temp fallback is bugsee-<uid>/<token>, 0700, symlink/owner checked; probe uses create_new with a random name - recover and deliver leftovers from the previous default location once - mid-run degradation is visible (StorageStatus::degraded); fork child falls back to memory instead of failing; warn_once per message, never panics - #[non_exhaustive] on the new public types; ffi test no longer writes to the real data directory 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- fork child that falls back to memory clears the panic snapshot path and updates the published storage resolution (dir None, source None) - legacy location is adopted only if <temp>/bugsee and the token dir are real directories owned by us and not our own directory (canonicalised compare) - legacy queue is included in flush, gc'd for orphans, and retired from polling once recovery has run and its queue is empty - do not resolve temp_dir() unless a default location is actually needed - tests: fork memory-fallback, symlinked/aliased legacy dir, end-to-end legacy crashed session + queued report delivered once and cleaned up 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…sharing a data dir Several processes on one data directory could double-deliver (or lose) reports: - A session was recovered twice: a peer that listed it before another finished claimed it after the winner had discarded it, producing a phantom report. The marker is now re-checked after the claim is won. - Stale-claim takeover was remove-then-create, so two takers could each remove the other's fresh claim and both hold it. Takeover is now serialised by a guard file and re-validated under it; release only removes the claim's own content. - Claims and session markers identified owners by bare pid: a recycled pid (or a restarted container's pid 1) kept a dead owner's work blocked forever. Owners now carry a process start time (Linux /proc, macOS proc_pidinfo, Windows GetProcessTimes), with a 30 minute lease as a backstop. - On Windows process liveness always answered "dead", so live peers' claims and markers were taken over. Implemented via OpenProcess/GetExitCodeProcess. - queue::gc_orphans at a peer's launch reaped another process's in-flight enqueue (sidecars before the bundle rename), stranding the bundle and losing the report. Only files untouched for 60 s are reaped now. - Leftover claims of discarded sessions are swept. Adds tests/multiprocess.rs (real child processes racing; SIGKILLed holder) plus unit tests for claims, process identity and queue gc. Closes #14 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…S file locks Review of the pid/start-time claim scheme found it unsound where #14 matters most: containers with separate pid namespaces sharing a volume (both apps pid 1) cannot compare pids or start times, so a peer's live claim was judged stale and its live session reported as a crash. Pid-in-file schemes also needed a lease, a takeover guard with a timer, and identity checks, each with its own race. Replace them with kernel locks: fcntl(F_SETLK) on unix (per process, released on death, not inherited by fork) and LockFileEx on Windows, on dedicated *.lock files (queue/<bundle>.lock, sessions/<gen>.recover.lock, and sessions/<gen>.lock held for a session's life). Nothing can go stale, so there is no lease, takeover or start-time logic. An in-process registry (pid-keyed, spin-locked with the holder's pid so a fork child cannot inherit a held lock) excludes threads and keeps a process from ever opening one lock file twice, which POSIX would punish by dropping the lock. - .alive stays a bare pid and lock files are new names, so older builds sharing a directory keep working (no exactly-once between old and new; documented). Markers with no .lock beside them still use the pid probe. - Liveness probe: zombies count as dead (Linux /proc state, macOS proc_pidinfo); Windows uses WaitForSingleObject. - Leftover lock files of finished items and dead sessions are swept when free. - Tests: a live peer whose marker names a colliding pid (parent's, init's) is not recovered until it really dies, then exactly once; concurrent claimers; probing never drops a held lock; zombie probe. Closes #14 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- an existing directory that fails the write probe / session open is still recovered from and drained (read + delete only); claims are lenient there and a crash that cannot be re-queued on disk is delivered from memory; delivered but undeletable bundles are remembered so they are not resent every drain - in-memory TooManySimilar blacklist (cleared in fork children) - a clean stop gives the in-memory backlog a last bounded delivery attempt - legacy temp location adopted whenever it exists and is trusted, even if no new default directory was usable - rotation tick rewrites a missing liveness marker after a wipe and marks the recorder degraded - DESIGN.md: failure table, legacy/flush wording 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- A launch that cannot hold <gen>.lock removes the file instead of leaving an unheld one, which every peer read as "that session died" and recovered (and deleted) the live session. Contended claims are retried briefly; an empty, fresh marker with a free lock is skipped (the pid is written after locking); on unix a probe uses F_GETLK and never takes the lock. - Filesystems that reject locks (ENOLCK, EOPNOTSUPP, ENOSYS, EINVAL) degrade to thread-only claims with a one-time message, and probes report unknown so recovery falls back to the marker pid, instead of blocking the queue forever. - An unclean shutdown (detached worker/uploader) keeps the session lock until the process exits so peers cannot recover a session still being written. - Registry keys are canonicalised; a fork child resets the registry from the fork hook; old-scheme .claim/.claim.takeover files are swept. - Tests: the real pid-namespace hazard (marker pid that does not exist here, reaped pid), crashed sessions with free lock files, the lock-race launch, unsupported-lock injection, and the probe/launch ordering cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Resolves conflicts in claim.rs (keep the file-lock claims, add the lenient variant #9 introduced: a directory that cannot take a lock file proceeds unclaimed) and recovery.rs (claim_pending is lenient and still re-checks the marker). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… unlinkable claims proceed (#9) - recovery_dirs: all rejected existing candidates (incl. the private temp tree) are drained/recovered even when a later default wins; core takes extra_data_dirs - a stale claim that cannot be unlinked is Unavailable, not Taken - seed the in-memory blacklist from the on-disk one 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Keeps the file-lock claim.rs (the pid-claim changes from #9 no longer apply; its lenient variant is retained). Writes the F_UNLCK comparison portably and gates the unix-only Lock::Unsupported variant so non-unix clippy is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review: exactly-once delivery (#14)
Reviewed 69a6bc8 against callers in
runtime,session,queue, and the #9 drain path this is stacked on — not the diff alone. The lock design is the right arbiter for pid namespaces when the kernel actually excludes, and the multiprocess tests cover the happy path. The holes are on the failure/fallback paths that #14 and #9 both care about.Findings
- P1 —
claim::probetreats a lock file it cannot open or inspect as Held. A leftoversessions/<gen>.lockon a remount-RO (or otherwise unopenable-for-write) volume makesfind_pendingskip that crash forever. This undoes #9's "still recover from a directory that cannot take new files" for every session this build itself writes. Windows never producesLock::Unsupportedeither (CIdead_code).- P1 — When the filesystem rejects locks,
take_lockdeletes<gen>.lockand recovery falls back topid == self_pidas proof of death. Two pid-1 containers on NFS/9p/FUSE will recover anddiscardeach other's live session. The write-up only mentions double-delivery.- P2 —
claim_pendingre-checks that the marker still exists, not that the session lock is still free. A launch that finishestake_lock+ pid write afterfind_pendingclassified the file as free is then discarded while live.CI (run 38552974115)
macos-latest SUCCESS. ubuntu-latest / android aarch64 clippy fail on
unnecessary_cast(F_UNLCK as i32). windows-latest clippy fails becauseLock::Unsupportedis never constructed. Tests did not run on those jobs.I did not run the workspace test suite here. I did confirm
OpenOptions::{read,write}fails on a 0444 lock file (EACCES) while read-only open succeeds — the probe flags this PR uses.Overall risk: High
Merge recommendation: request changes.
Most important to fix: (1) do not map probe/lock inspection failure to Held — use
Unknown+ the pid fallback, and open unix probesO_RDONLY; (2) on lock-less filesystems do not treatpid == self_pidas death (or keep a kernel-visible liveness signal); (3) re-probe<gen>.lockunder the recovery claim.What is in good shape
fcntl-not-flock so a prefork child does not inherit the parent's claim; registry-before-open so POSIX cannot drop a held lock on a second close; marker re-check after winning
recover.lock(the listed phantom-duplicate); 60s queue orphan grace;keep_lock_until_exitfor a detached worker; real child-process tests for racing launchers, SIGKILL, and a colliding marker pid. Those are the right tests for the path where locks work.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (6267bd5)
Re-review after
Merge origin/fix/issue-9; portable clippy fixes for lock claims. Compared with 69a6bc8: clippy/-D warningson Linux, Android, and Windows is fixed and CI 38554556769 is green. The two P1s and the recovery TOCTOU are unchanged. I did not runcargo testlocally; the permission probe for leftover locks was checked on this host (O_RDWRon a 0444 file →EACCES;O_RDONLYsucceeds; a 0555 directory with a 0644 lock file still opens RDWR).The lock-based scheme is the right arbiter for pid namespaces, and the multiprocess tests (racing launchers, SIGKILL, colliding marker pids) cover the happy path. The remaining holes are on the degrade/legacy paths this PR itself introduces, and they can lose a crash or delete a live session.
Findings
P1 — leftover
<gen>.lockthat cannot be opened for write is treated as a live session (claim.rsprobe)
probeopens RDWR and maps any open/lock error toHeld.find_pendingthencontinues. Every crash this build leavessessions/<gen>.lockbehind (unlocked). On a remount-RO volume — the case #9 / DESIGN.md §8 promised to still drain — that leftover file hides the crash permanently. Queue drain is still lenient; session discovery is not. The #9 leftovers test would not catch this even with a seeded.lock: it only chmods directories to 0555, and an existing 0644 lock file still opens RDWR.P1 — lock-less fallback treats
pid == self_pidas death (recovery.rs+Session::take_lock)When the filesystem rejects locks,
take_lockdeletes<gen>.lock, so peers never take the lock-arbitrated branch.legacy_marker_is_deadthen treats a marker bearing this process's pid as a crashed predecessor. Two pid-1 containers on NFS-without-lockd / FUSE / 9p (the #14 scenario) recover anddiscardeach other's live session. Documented degrade is double-delivery; this is live-session deletion plus a phantom crash.P2 —
claim_pendingre-checks marker existence only (recovery.rs)
find_pendingcan classify Free (unixF_GETLKbeforeF_SETLK, or Windows take-then-release) and then see a pid written after the lock is held. The later claim only asks whether.alivestill exists, which it does for a live launch.recover_one+discardthen tear down that session. The gap is one RPC on NFS, and larger when other pendings are recovered first.P2 — Windows still has no
Unsupportedpath (claim.rsos::try_lock)Clippy
dead_codeis silenced withallow;try_lockstill maps onlyERROR_LOCK_VIOLATIONtoContendedand everything else toErr→ probeHeld. The documented thread-only degrade does not exist on Windows.Overall risk: High
Merge recommendation: Request changes
Do not merge until leftover
.lockfiles cannot hide crashes on an unwritable volume, and the lock-less pid fallback cannotdiscarda live peer in another pid namespace.Most important to fix
- Unix probe
O_RDONLY; map open/lock errors toProbe::Unknown(pid fallback), notHeld. Seed a 0444 leftover.lockin the #9 leftovers test (0555 dirs alone are not enough).- On the lock-less path, do not treat same-pid as death; stop deleting
<gen>.lockon!os_lockedso peers seeUnknowninstead of a bare pid.- Under the recovery claim, re-probe
sessions/<gen>.lockand abort ifHeld.What looks solid
fcntl/LockFileExas the cross-namespace arbiter, the in-process registry (canonicalised, pid-stamped spinlock, close-before-unregister),queue::gc_orphans60s grace, marker re-check after winningrecover.lock,keep_lock_until_exitfor detached shutdown, and real child-process tests inmultiprocess.rs. Lenient claims from #9 are kept. CI is green on macos/ubuntu/windows plus Android/iOS lint.Sent by Cursor Automation: Bugsee code review
- A leftover <gen>.lock that cannot be opened (0444, read-only volume) or whose lock call errors probes as Unknown, not Held, and the probe opens read-only, so it can no longer hide a crash forever. - Lock-less sessions (filesystem rejects locks) keep a <gen>.beat heartbeat file, touched every 2 s by the worker; a beat younger than 60 s means alive whatever pid the marker names, so a peer in another pid namespace is never mistaken for a crash. Without a beat the pid is probed as before. - claim_pending re-runs the liveness decision under the recovery claim, so a session that finished locking since the scan is not torn down. - Windows maps ERROR_INVALID_FUNCTION/NOT_SUPPORTED/CALL_NOT_IMPLEMENTED to Unsupported, so the documented degrade exists there; the dead-code allow is gone. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep code review (ee3f808)
Round 3 (
fix(core): review round 3 on lock claims) hits the three holes from 6267bd5:
- P1 leftover
.lock→ Held — fixed. Probe opensO_RDONLY; open/lock errors areUnknown; 0444 leftover lock unit-tested.- P2
claim_pendingmarker-only — fixed. Re-runssession_is_crashedunder the recovery claim; covered by a lock-taken-since-scan test.- P2 Windows
Unsupported— fixed.ERROR_INVALID_FUNCTION/NOT_SUPPORTED/CALL_NOT_IMPLEMENTEDnow degrade as documented; thedead_codeallow is gone.The lock-less pid-1 case now has a
<gen>.beatheartbeat (write at launch, touch every 2s, 60s stale). That is the right signal when there is no kernel lock. One residual on that path still lets a live peer bediscarded on NFS — the volume this fallback exists for.I have not seen CI for ee3f808 yet and did not run
cargo testhere.Remaining finding
P2 — heartbeat freshness treats future/unreadable mtimes as dead, and
age < 60shas no headroom over NFSacregmax
legacy_marker_is_deadusest.elapsed().ok()thenage < HEARTBEAT_STALE(60s). Forty lines later,marker_is_freshtreats future-dated and unreadable mtimes as fresh (unwrap_or(true)) specifically so a live peer is never disturbed. The beat does the opposite: any mtime in the future (client clock behind the NFS server) skips the heartbeat and falls through topid == self_pid.Linux NFS defaults
acregmax=60. The comment calls 60s generous because of that cache;age < 60sis then exactly the cache ceiling with no margin, so a live beat can look stale and the same pid fallback runs.Overall risk: Medium
Merge recommendation: Request changes
The lock-arbitrated path is in good shape. Do not merge until the lock-less heartbeat cannot classify a live NFS peer as dead.
Most important to fix
Match
marker_is_fresh: future/unreadable beat mtime → alive. SetHEARTBEAT_STALEwell above NFSacregmax(e.g. 180s) or useage <=with explicit headroom.What looks solid
O_RDONLY probe,
session_is_crashedshared by scan and claim, Windows unsupported-lock mapping, beat created only whentake_lockfails,touch_heartbeatindependent ofrotate_interval, discard/end/gc of.beat, and tests for 0444 leftover locks, colliding-pid + fresh beat, and claim-after-lock.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (ee3f808)
Re-review after
fix(core): review round 3 on lock claims. Compared with 6267bd5: the three concrete holes are patched — unix probe isO_RDONLYand maps errors toUnknown;claim_pendingre-runssession_is_crashed; Windows mapsERROR_INVALID_FUNCTION/NOT_SUPPORTED/CALL_NOT_IMPLEMENTEDtoUnsupported. CI 38555911058 is green on ubuntu/macos/windows tests plus Android/iOS lint. I did not runcargo testlocally.The lock-capable path looks right. The remaining hole is the lock-less heartbeat that was added to close the pid-1 NFS case: freshness is computed with
modified().elapsed().ok(), which fails closed on the filesystem this fallback is for.Findings
P1 — lock-less heartbeat is ignored when mtime is in the future or ≥ 60s old (
recovery.rslegacy_marker_is_dead)A beat counts as alive only when
elapsed()succeeds andage < 60s. Any future mtime (elapsed()→Err) is treated as no heartbeat, thenpid == self_pidis death.marker_is_freshin the same file already useselapsed().unwrap_or(true)so a possibly-live peer is never disturbed; the beat does the opposite.NFS mtime is the server's clock.
elapsed()compares it to the container'sSystemTime::now(). If the server is ahead by a millisecond, the beat is dead on arrival for the entire session — not a boot-time NTP blip. Linuxacregmaxalso defaults to 60s, so a cached live mtime can land onage < 60s == false.P2 — the worker does not touch the beat until
run_recoveryfinishes (runtime.rs)
Session::beginwrites<gen>.beat, thenspawn_pipelinerunsrun_recovery(and legacy dirs, and a hostbefore_send) beforeworker_loop. The firsttouch_heartbeatis another 2s after that. DESIGN.md says the worker touches every 2s; until the loop starts the file just ages. A lock-less launch whose recovery exceeds ~60s looks like a crash to a peer that starts in that window.Overall risk: High
Merge recommendation: Request changes
Do not merge until a lock-less live session on NFS/9p/FUSE cannot be
discarded because of clock or attribute-cache skew. The kernel-lock path is fine.Most important to fix
- Treat unreadable/future beat mtime as alive (same shape as
marker_is_fresh). Do not fall through topid == self_pidunless the beat is demonstrably quiet. MakeHEARTBEAT_STALEstrictly larger than NFSacregmax(180s is the usual advice).- Touch (or at least create) the beat on the launch path before
run_recovery, and keep touching it while recovery runs — or run recovery after the heartbeat loop is armed.What looks solid
Round-3 fixes for leftover RO
.lockfiles,claim_pendingre-checking liveness underrecover.lock, and a real WindowsUnsupportedbranch.fcntl/LockFileExas the cross-namespace arbiter, the in-process registry, 60s queue-orphan grace,keep_lock_until_exit, and the multiprocess tests (racing launchers, SIGKILL, colliding marker pids) still cover the path where locks work. CI is green.Sent by Cursor Automation: Bugsee code review
… launches - A heartbeat whose mtime is in the future (file server clock ahead) counts as fresh, like marker_is_fresh; only a demonstrably quiet beat condemns a session. The staleness window is 3 minutes, above NFS attribute-cache lifetimes. - Launch-time recovery (before the worker loop starts) now keeps the beat going. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep code review (c4f81d6)
Round 4 (
fix(core): make the lock-less heartbeat robust to clock skew and slow launches) closes the remaining lock-less holes.
- P2 future-dated / 60s-stale beat — fixed.
elapsed().unwrap_or(true)matchesmarker_is_fresh;HEARTBEAT_STALEis 180s (above Linux NFSacregmax); future-dated beat is unit-tested.- P2 beat idle during
run_recovery— fixed.touch_heartbeatruns at the start of recovery and before each pending, so a long launch-time scan no longer lets the beat go quiet under the 3-minute window.Earlier rounds already landed O_RDONLY probe →
Unknown,session_is_crashedunder the recovery claim, and WindowsUnsupported. I did not runcargo testhere; CI on ee3f808 was green.Findings
No remaining P0–P3 with a credible failure scenario. Residual only:
ensure_markerstill rewrites.aliveafter a wiped tree and does not recreate a missing.beat, so a lock-less session would fall back to pid until the next launch writes a new beat. That stack (wipe + lock-less + pid-namespace peer) is not worth blocking on.Overall risk: Low
Merge recommendation: Approve
What looks solid
Kernel locks as the pid-namespace arbiter; registry-before-open; marker re-check plus liveness re-check under
recover.lock; 60s queue orphan grace;keep_lock_until_exit; heartbeat that is conservative on clock skew and NFS attribute cache; real child-process tests for racing launchers, SIGKILL, colliding marker pids, 0444 leftover locks, and claim-after-lock.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (c4f81d6)
Re-review after
fix(core): make the lock-less heartbeat robust to clock skew and slow launches. Both leftover findings from ee3f808 are fixed. I did not runcargo testlocally; CI for this SHA was still in progress (MSRV and Android lint already green).Prior findings
- P1 leftover
.lock→ Held — still fully_addressed (O_RDONLY+Unknown).- P2
claim_pendingmarker-only — still fully_addressed.- P2 Windows
Unsupported— still fully_addressed.- P1 heartbeat
elapsed().ok()fail-closed — fully_addressed. Future mtime is now alive (unwrap_or(true), same shape asmarker_is_fresh).HEARTBEAT_STALEis 180s, above NFSacregmax. Covered bya_future_dated_heartbeat_counts_as_alive.- P2 beat quiet during
run_recovery— fully_addressed. Recovery touches this session's beat before the scan and before each pending.No remaining P0–P3. Residual only: a single
recover_one/ blockingbefore_sendthat runs longer than three minutes still would not refresh the beat;ensure_markerstill restores.alivewithout recreating.beatafter a wiped tree. Neither has a credible failure on the NFS/pid-1 path this round closed.Overall risk: Low
Merge recommendation: Approve
Most important to fix
Nothing blocking. Land after CI for c4f81d6 is green (and after #24, which this is stacked on).
What looks solid
Kernel locks as the cross-namespace arbiter, registry-before-open, lenient claims from #9, 60s queue-orphan grace,
keep_lock_until_exit, and the multiprocess tests. The lock-less fallback now fails open on clock skew and stays alive through launch-time recovery.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (c4f81d6)
Round 4 (
fix(core): make the lock-less heartbeat robust to clock skew and slow launches) closes both leftovers from ee3f808:
- P1 future/unreadable beat mtime — fixed.
elapsed().unwrap_or(true),HEARTBEAT_STALEis 180s, unit test for a future-dated beat.- P2 no touch until
worker_loop— fixed.run_recoverytouches the beat at entry and once per pending, including while draining legacy dirs.CI 38557397063 is green (ubuntu/macos/windows tests, Android/iOS lint, MSRV). I did not get a local
cargo testto run here (sandbox Cargo 1.83 cannot parsehashbrown 0.17.1).The lock-capable path and the clock-skew heartbeat check look right. One hole is left on the lock-less NFS path this heartbeat exists for:
session_is_crashedstill treatsProbe::Freeas “holder died” even when that Free only means “the path is gone.”Findings
P1 — a vanished
<gen>.lockis classified as a crashed session, so the heartbeat is never consulted (recovery.rssession_is_crashed)
take_lockalways unlinks the lock file when the filesystem rejects locks, and it does that before.beatis written.probemaps “existed, then ENOENT” toProbe::Free.session_is_crashedthen returns “dead” unless the marker is still empty-and-fresh — it never reacheslegacy_marker_is_dead.Overall risk: High
Merge recommendation: Request changes
Do not merge until a lock-less live session on NFS/9p cannot be
discarded because a peer still “sees” the lock file thattake_lockjust removed. The kernel-lock path and the clock-skew heartbeat fix are fine.Most important to fix
- Do not treat
Probe::Freeas death unless the lock file is still there (vanished → fall through to the beat).probeshould not equate ENOENT with “unlocked because the holder died.”- On
Unsupported, keep<gen>.lockinstead of unlinking it. Peers then getProbe::Unknownand use the beat, which is the whole point of not unlinking a file that cannot be locked.What looks solid
Future mtime → alive, 180s vs
acregmax, beat during launch recovery, O_RDONLY probe,claim_pendingre-runningsession_is_crashed, WindowsUnsupportedmapping, in-process registry, 60s queue-orphan grace,keep_lock_until_exit, and the multiprocess tests (racing launchers, SIGKILL, colliding marker pids). CI is green.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (c4f81d6) — correction
The clock-skew / 180s / launch-time
beat()work from this SHA is still good, and the earlier probe/claim_pending/Windows fixes still hold. I am withdrawing the approve.
session_is_crashedtrustsProbe::Freeas “the holder died” wheneverlock.exists()was true.probealso returnsFreewhen the path vanishes between those checks (exists()then ENOENT). On the lock-less pathtake_lockunlinks<gen>.lockbefore writing.beat. A peer that observed the file can therefore classify a live session as crashed and never look at the heartbeat.That is a TOCTOU locally and a cached-lookup hole on NFS (
acdirmax), which is the volume this beat exists for. Steady-state after a cold lookup (file already gone, no cache) does use the beat; the hole is the exists→vanished path.Findings
P2 —
Probe::Freeafter a vanished lock skips the heartbeat (session_is_crashed)Overall risk: Medium
Merge recommendation: Request changes
Do not merge until a vanished
<gen>.lockfalls through tolegacy_marker_is_dead(the beat), ortake_lockstops unlinking onUnsupportedso peers getUnknown.Most important to fix
If
probereturnsFree, re-check that the lock file is still present; if it is gone, use the beat. Keeping the unlinked-on-Unsupported file is an alternative: fcntl then reportsUnknown.What still looks solid
Future mtime → alive, 180s vs
acregmax, beat duringrun_recovery, O_RDONLY probe, WindowsUnsupported,claim_pendingre-running liveness. CI 38557397063 was green.Sent by Cursor Automation: Bugsee code review
- legacy_is_adoptable compares against the chosen dir AND recovery_dirs by canonical path (Windows: temp fallback is the legacy location) - spawn path dedups legacy/extra dirs against the data dir and each other - seed the in-memory blacklist from legacy/extra dirs too - test: an existing temp tree is offered in exactly one role 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…a crash probe returned Free both for an unheld lock and for a file that disappeared between a caller's exists() and the open. A session without kernel locks unlinks its <gen>.lock before writing its heartbeat, so a peer that saw the file could classify that live session as crashed. probe now reports Probe::Vanished for a missing file (no pre-check), and session_is_crashed treats it like Unknown and falls through to the heartbeat/pid decision. Test injects the vanish between the existence check and the probe's open. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep code review (3b90a28)
fix(core): a lock file that vanishes under a scan is not evidence of a crashcloses the exists→ENOENT hole.
probeno longer pre-checksexists()or maps a missing file toFree. OpenNotFoundisProbe::Vanished.session_is_crashedtreatsVanishedlikeUnknownand falls through to the heartbeat/pid path.- Test unlinks the file inside
BEFORE_PROBE_OPEN, which is the race the last round described.ESTALE (NFS stale handle) still becomes
Unknownvia the generic open/lockErrarm, so it also uses the beat. I did not runcargo testhere.Findings
No remaining P0–P3.
Overall risk: Low
Merge recommendation: Approve
Stacked on #24. Land after CI for 3b90a28 is green.
What looks solid
Kernel locks as the pid-namespace arbiter;
VanishedvsFree; heartbeat that fails open on clock skew (180s); beat during launch recovery; O_RDONLY probe; WindowsUnsupported;claim_pendingre-running liveness; multiprocess tests.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (3b90a28)
Re-review after
fix(core): a lock file that vanishes under a scan is not evidence of a crash. The last remaining hole is closed. I did not runcargo testlocally; CI for this SHA was still in progress (MSRV and Android lint already green).Prior findings
- Leftover RO
.lock→ Held,claim_pendingTOCTOU, WindowsUnsupported— still fully_addressed.- Heartbeat future-mtime / 180s / launch-time
beat()— still fully_addressed.- P2 vanished
<gen>.locktreated asProbe::Free— fully_addressed.probenow returnsVanishedonNotFound(no more exists-then-ENOENT → Free).session_is_crashedfalls through to the beat onVanished | Unknown. Covered bya_lock_file_that_vanishes_under_the_scan_is_not_evidence_of_a_crash.take_lockstill unlinks onUnsupported;Vanishedis what makes that safe.No remaining P0–P3.
Overall risk: Low
Merge recommendation: Approve
Nothing blocking. Land after CI for 3b90a28 is green, and after #24 (this is stacked on
fix/issue-9).What looks solid
VanishedvsFreeis the right split: an unheld leftover file is still a crash; a path that is gone is not. Kernel locks, registry-before-open, heartbeat clock-skew handling, beat during recovery, and the multiprocess tests remain in good shape.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (3b90a28)
Round 5 (
fix(core): a lock file that vanishes under a scan is not evidence of a crash) closes the leftover from c4f81d6.
- P1/P2 vanished
.lock→Probe::Freeskips the heartbeat — fixed.probeno longer pre-checksexists(). OpenENOENTisProbe::Vanished, not Free.session_is_crashedtreats Vanished like Unknown and falls through tolegacy_marker_is_dead. Covered bya_lock_file_that_vanishes_under_the_scan_is_not_evidence_of_a_crash(unlink injected betweenexists()andopen).Walked the NFS exists-then-gone path against current code: cached positive dentry →
exists() == true→openENOENT/ESTALE→ Vanished or Unknown → beat. On a lock-less FS a leftover file that does open still peeksUnsupported→ Unknown, so the beat is used even if unlink lagged. Earlier rounds already landed O_RDONLY → Unknown,session_is_crashedunderrecover.lock, WindowsUnsupported, future-mtime/180s beat, and launch-timetouch_heartbeat.CI 38560132280: ubuntu/windows tests, Android/iOS lint, and MSRV green; macos-latest still running. I did not run
cargo testhere (sandbox Cargo 1.83 cannot parsehashbrown 0.17.1).Prior findings
- P1 leftover
.lock→ Held — still fully_addressed.- P2
claim_pendingmarker-only — still fully_addressed.- P2 Windows
Unsupported— still fully_addressed.- P1 heartbeat
elapsed().ok()fail-closed — still fully_addressed.- P2 beat quiet during
run_recovery— still fully_addressed.- P1/P2 vanished lock skips beat — fully_addressed (this SHA).
No remaining P0–P3 with a credible failure scenario. Residual only:
ensure_markerstill restores.alivewithout recreating.beatafter a wiped tree; a singlerecover_one/ blockingbefore_sendlonger than three minutes would not refresh the beat; queue/empty-marker graces still assume clocks within seconds (DESIGN.md). None of those is the NFS exists-then-unlink hole this round closed.Overall risk: Low
Merge recommendation: Approve
Most important to fix
Nothing blocking. Land after macos CI for 3b90a28 is green (and after #24, which this is stacked on).
What looks solid
Probe::Vanishedvs Free is the right split: an unheld leftover is still a crash, a missing path is not. Kernel locks as the pid-namespace arbiter; registry-before-open; liveness re-check underrecover.lock; 60s queue-orphan grace;keep_lock_until_exit; heartbeat that fails open on future mtimes and stays alive through launch recovery; real child-process tests for racing launchers, SIGKILL, colliding marker pids, leftover 0444 locks, claim-after-lock, and now a vanished lock with a live beat.Sent by Cursor Automation: Bugsee code review
…es (#9) One retire_queued helper for all arms (delivered, TooManySimilar, duplicate, permanent, retries exhausted); recovered crashes consult the in-memory blacklist. Test: terminal verdicts on a read-only queue are sent once. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep code review (e01a20a)
Re-review after
Merge origin/fix/issue-9 into fix/issue-14. The #14 lock/claim/heartbeat path is unchanged from 3b90a28 (claim.rs/recovery.rs/session.rs/multiprocess.rshave an empty diff).Probe::Vanished, future-mtime beats, launch-timetouch_heartbeat, and the earlier probe/claim_pending/Windows fixes still stand.The merge itself does not build.
Findings
P1 — duplicate
[target.'cfg(unix)'.dependencies]incrates/bugsee/Cargo.tomlTwo unix-dependency tables (always-on
libcat line 20, optionallibcat line 48 from #27). Cargo 1.88:duplicate key \dependencies` in table `target.cfg(unix)``. CI 38577790777 is red on MSRV, ubuntu, windows, and Android lint in ~20s. I reproduced the load failure locally; I did not run the test suite.
runtime.rschanges from #9 (distinct_extra_dirs,retire_queued, mem-blacklist onrecover_one) look compatible with exactly-once: extras are not recovered twice, andbeat_markeris still passed intorun_recovery.Overall risk: High
Merge recommendation: Request changes
Do not merge until the workspace loads. The #14 design is fine; this SHA is a broken merge.
Most important to fix
Collapse the two unix
libctables into one (optional, gated ontelemetry, matching #27).What looks solid
The exactly-once work from 3b90a28 is intact. #9's extra-dir dedup is the right companion for not delivering a recovered session twice.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (e01a20a)
Re-review after
Merge origin/fix/issue-9 into fix/issue-14. Unique #14 lock files (claim.rs,recovery.rs,session.rs,queue.rs,multiprocess.rs) are byte-identical to 3b90a28. Prior lock findings (leftover-RO probe, lock-lesspid == self_pid,claim_pendingTOCTOU, WindowsUnsupported, heartbeat clock-skew / 180s, vanished lock →Probe::Vanished) remain fully_addressed.The merge does keep the lock contract: extra-dir recovery still passes
beat_marker,distinct_extra_dirsstill avoids recovering the same tree twice, andretire_queuedstill only remembers undeletable bundles in this process (cross-process re-delivery on a read-only queue remains the documented lenient degrade).Findings
P0 Critical —
crates/bugsee/Cargo.toml:20and:48Problem: The merge imported a duplicate table header
[target.'cfg(unix)'.dependencies]. Line 20 has requiredlibc = "0.2"(#9,geteuid/ data-dir ownership). Line 48 has optionallibc = { version = "0.2", optional = true }(#27 hostinfo, landed on the base viaMerge origin/main into fix/issue-9). Cargo rejects duplicate keys in the same table.Impact: The workspace manifest does not load. Nothing compiles, lints, or tests. This PR cannot merge and #14 is unreviewable in CI until the table is unique.
Scenario: Confirmed on this host and on CI run 38577790777: every job fails in ~20s with
duplicate key \dependencies` in table `target.cfg(unix)`at line 48.cargo metadata` locally:error: invalid table header duplicate key `dependencies` in table `target.cfg(unix)` --> crates/bugsee/Cargo.toml:48:1The file is identical to
origin/fix/issue-9(b6ce5e0), so the same break is on #24; this merge made it HEAD of #32. It is not in the unique three-dot diff, which is why there is no inline comment.Fix: Keep one unix
libcentry. It must be required (libc = "0.2"):options.rs(geteuid) andhostinfo.rs(localtime_r/sysctlbyname) use it without thetelemetryfeature. Drop"dep:libc"fromtelemetry(that syntax is only valid for an optional dep, and libc cannot be optional here). Then either fix it on #24 and re-merge, or the same one-line cleanup on this branch.CI (run 38577790777)
All eight jobs failure (ubuntu/macos/windows tests, MSRV, Android lint, both iOS lints, macOS private-API check). I did not run the workspace test suite; Cargo never gets past the manifest. I did confirm the duplicate-key parse error locally.
Overall risk: High
(The lock logic is still Low. The merge commit is not mergeable.)
Merge recommendation: Request changes
Do not merge until
cargo metadatasucceeds. No remaining unique #14 correctness holes at this SHA.Most important to fix
- Deduplicate
crates/bugsee/Cargo.tomlunixlibcas above so CI can run.What still looks solid
Probe::Vanished+ beat/pid fallback, launch-timebeat()during current and extra-dir recovery,claim_pendingre-runningsession_is_crashed,keep_lock_until_exit,reset_after_fork, and the multiprocess tests.retire_queuedcorrectly extends in-process undeletable-bundle tracking to every terminal verdict (the flush storm #9 fixed); extra-dir blacklist seeding plusmem_blacklistedinrecover_onedoes not reopen the live-session path.Residual (unchanged, accepted):
ensure_markerstill does not recreate.beat; a singlerecover_onethat blocks >180s can starve the beat; queue/empty-marker graces still assume clocks within seconds.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (e01a20a)
Re-review after
Merge origin/fix/issue-9 into fix/issue-14. Unique #14 files (claim.rs,recovery.rs,session.rs,queue.rs,process.rs,multiprocess.rs) are unchanged from 3b90a28. Walked the remarge intersection inruntime.rs(distinct_extra_dirs,retire_queued, legacymem_blacklistseed) against the claim/heartbeat paths — those helpers do not reopen the lock-less live-session holes.I fetched CI 38577790777 logs. I did not run
cargo testhere (sandbox rustc 1.83 cannot parsehashbrown 0.17.1).Prior findings
- P1 leftover
.lock→ Held — still fully_addressed (O_RDONLY+Unknown).- P2
claim_pendingmarker-only — still fully_addressed.- P2 Windows
Unsupported— still fully_addressed.- P1 heartbeat
elapsed().ok()fail-closed — still fully_addressed.- P2 beat quiet during
run_recovery— still fully_addressed.- P1/P2 vanished lock skips beat — still fully_addressed (
Probe::Vanished).Finding
P0 Critical — remarge left two
[target.'cfg(unix)'.dependencies]tables incrates/bugsee/Cargo.tomlLocation:
crates/bugsee/Cargo.tomllines 20–21 and 46–49 (identical tob6ce5e0; not in the 3-dot unique diff).Problem: The file declares
[target.'cfg(unix)'.dependencies]twice:libc = "0.2"(required, this branch) andlibc = { version = "0.2", optional = true }plustelemetry = ["dep:sysinfo", "dep:libc"](from #27 on the remarged base). Cargo rejects a duplicate table key and will not load the workspace member.Impact: Every
cargoinvocation fails before compile. CI 38577790777 is 8/8 FAILURE on the first cargo step (fmt, build, clippy, MSRV, Android, both iOS targets, macOS private-API). The #14 lock work cannot be re-verified on this SHA.Scenario:
e01a20amergedorigin/fix/issue-9(b6ce5e0, itself a remarge of main/#27). Both sides already had a unix-libc table; the resolution kept both.3b90a28(previous head) had a single table and built.options.rscallslibc::geteuid()on every unix launch (private temp name / ownership), so libc must stay a required unix dep — the second, optional table is the leftover.Fix: Keep one
[target.'cfg(unix)'.dependencies]withlibc = "0.2". Drop the second table and changetelemetryback to["dep:sysinfo"](dep:libcis redundant once libc is always present on unix). Same fix belongs onfix/issue-9if that tip is stillb6ce5e0.Overall risk: High
Merge recommendation: Request changes
Do not merge while the crate does not load. The #14 lock design is still sound; this SHA is not.
Most important to fix
- Collapse the duplicate unix
libctables so Cargo accepts the workspace, then re-run CI.What still looks solid
Kernel locks as the pid-namespace arbiter;
Probe::Vanishedvs Free; heartbeat that fails open on future mtimes (180s) and is touched during launch recovery;O_RDONLYprobe → Unknown;claim_pendingre-runningsession_is_crashed; WindowsUnsupported;keep_lock_until_exit; 60s queue-orphan grace; remarged #9 drain helpers (retire_queued,distinct_extra_dirs) do not bypass claims. Residual only:ensure_markerstill restores.alivewithout recreating.beat; a singlerecover_onelonger than three minutes would not refresh the beat. Neither is the compile break.Sent by Cursor Automation: Bugsee code review
…optional form vs the unconditional one) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep code review (fbbabb2)
Re-review after
Merge origin/fix/issue-9 into fix/issue-14(1ecfca8de-duplicates unixlibc). Unique #14 lock files (claim.rs,recovery.rs,session.rs,queue.rs,multiprocess.rs) are still byte-identical to 3b90a28. Extra-dir recovery still passesbeat_marker;distinct_extra_dirs/retire_queued/mem_blacklisteddo not reopen the live-session path.Prior findings
- Leftover
.lock→ Held, lock-lesspid == self_pid,claim_pendingTOCTOU, WindowsUnsupported— still fully_addressed.- Heartbeat
elapsed()fail-closed / 180s / beat duringrun_recovery— still fully_addressed.- Vanished lock skips beat — still fully_addressed (
Probe::Vanished).- P0 duplicate
[target.'cfg(unix)'.dependencies]— fully_addressed. One required unixlibc = "0.2";telemetryis['dep:sysinfo'](nodep:libc).cargo metadataloads.--no-default-featuresstill hasgeteuid/hostinfolibc. Confirmed locally: one unix-deps table, workspace member loads.No remaining P0–P3.
CI (run 38579223977)
MSRV 1.88 and Android aarch64 lint SUCCESS at review time (the previous SHA died before compile). ubuntu / macos / windows tests and iOS lints still running. I did not run the workspace test suite here (sandbox rustc 1.83 cannot parse
hashbrown 0.17.1).Overall risk: Low
Merge recommendation: Approve
The #14 lock design is intact and the stack merge now loads.
Most important issues to fix
None remaining.
What still looks solid
Kernel locks as the pid-namespace arbiter;
O_RDONLYprobe → Unknown;Probe::Vanishedvs Free; heartbeat that fails open on future mtimes and is touched during launch recovery;claim_pendingre-runningsession_is_crashed; WindowsUnsupported;keep_lock_until_exit; 60s queue-orphan grace. Residual (accepted):ensure_markerstill restores.alivewithout recreating.beat; a singlerecover_onelonger than three minutes would not refresh the beat.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (fbbabb2)
Re-review after
1ecfca8/ remarge oforigin/fix/issue-9. The duplicate unixlibctables are gone: one[target.'cfg(unix)'.dependencies]with requiredlibc = "0.2", andtelemetry = ["dep:sysinfo"](nodep:libc). That is the right shape —options.rsgeteuidmust work without the telemetry feature.I reproduced the load locally:
cargo +1.88 metadataandcargo +1.88 check -p bugsee-core -p bugseeboth succeed. CI MSRV and Android lint are already green on this SHA; ubuntu/windows tests were still in progress.Unique #14 files (
claim.rs,recovery.rs,session.rs,multiprocess.rs) are still byte-identical to 3b90a28. All prior lock/heartbeat findings remain fully_addressed.No remaining P0–P3.
Overall risk: Low
Merge recommendation: Approve
Land after the remaining CI jobs for fbbabb2 are green, and after #24 (this is stacked on
fix/issue-9).What looks solid
Kernel locks as the pid-namespace arbiter;
Probe::Vanishedvs Free; heartbeat that fails open on future mtimes (180s) and is touched during launch recovery;claim_pendingre-running liveness; WindowsUnsupported; #9 extra-dir dedup andretire_queuedstill compatible with exactly-once.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep code review (fbbabb2)
Re-review after
Merge origin/fix/issue-9 into fix/issue-14(second parent 1ecfca8, the unix-libcde-dupe). Unique #14 files (claim.rs,recovery.rs,session.rs,queue.rs,process.rs,multiprocess.rs) are still byte-identical to 3b90a28. Walked the remarge intersection inruntime.rs(distinct_extra_dirs,retire_queued, extra-dirmem_blacklistseed) against the claim/heartbeat paths — those helpers still do not reopen the lock-less live-session holes.I confirmed
cargo metadata --offlineloads the workspace on this host (Cargo 1.83). I did not run the test suite locally (sandbox rustc is 1.83; MSRV is 1.88). CI 38579223977: ubuntu/macos/windows tests, MSRV, Android lint, and the macOS private-API check are SUCCESS; both iOS lint jobs were still running at review time.Prior findings
- P1 leftover
.lock→ Held — still fully_addressed (O_RDONLY+Unknown).- P2
claim_pendingmarker-only — still fully_addressed.- P2 Windows
Unsupported— still fully_addressed.- P1 heartbeat
elapsed().ok()fail-closed — still fully_addressed.- P2 beat quiet during
run_recovery— still fully_addressed.- P1/P2 vanished lock skips beat — still fully_addressed (
Probe::Vanished).- P0 duplicate
[target.'cfg(unix)'.dependencies]— fully_addressed. One unix table with requiredlibc = "0.2";telemetry = ["dep:sysinfo"].options.rsgeteuidstill has a required unix libc. The file matchesorigin/fix/issue-9(1ecfca8).Findings
No remaining P0–P3 with a credible failure scenario.
Overall risk: Low
Merge recommendation: Approve
Nothing blocking. Land after #24 (this is stacked on
fix/issue-9).Most important to fix
Nothing on this SHA.
What still looks solid
Kernel locks as the pid-namespace arbiter;
Probe::Vanishedvs Free; heartbeat that fails open on future mtimes (180s) and is touched during launch recovery;O_RDONLYprobe → Unknown;claim_pendingre-runningsession_is_crashed; WindowsUnsupported;keep_lock_until_exit; 60s queue-orphan grace; remarged #9 drain helpers (retire_queued,distinct_extra_dirs) do not bypass claims. Multiprocess tests still cover racing launchers, SIGKILL, colliding marker pids, and a lost lock-race launch.Residual (unchanged, accepted):
ensure_markerstill restores.alivewithout recreating.beat; a singlerecover_one/ blockingbefore_sendlonger than three minutes would not refresh the beat; queue/empty-marker graces still assume clocks within seconds.Sent by Cursor Automation: Bugsee code review
Tree unchanged: this branch already contains #24's content. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Deep code review (deead02)
Re-review after Merge origin/main (squash of #24) into fix/issue-14. The commit message is accurate: the tree is identical to last-reviewed fbbabb2 (fbbabb2^{tree} == deead02^{tree}). Unique #14 files (claim.rs, recovery.rs, session.rs, queue.rs, process.rs, multiprocess.rs) are still byte-identical to 3b90a28.
This PR is already merged (6ceb6c8). The review below is of the synchronize SHA that landed.
Walked the lock path against callers, not the diff alone: claim::probe / acquire / registry / fork reset, session_is_crashed + legacy_marker_is_dead, claim_pending re-liveness under recover.lock, Session::take_lock (unlink-on-fail + beat), keep_lock_until_exit, Recorder::revive_after_fork (parent session leaked, reset_after_fork before new claims), run_recovery beat during the scan, drain_disk lenient queue claims, queue::gc_orphans 60s grace, Windows Unsupported mapping, and the real-child tests in multiprocess.rs.
cargo metadata --offline --no-deps loads on this host (Cargo 1.83). I did not run the workspace test suite (sandbox rustc is 1.83; MSRV is 1.88).
Prior findings
- P1 leftover
.lock→ Held — still fully_addressed (O_RDONLY+Unknown). - P1 lock-less
pid == self_pidas death — still fully_addressed (<gen>.beat, 180s, future mtime alive). - P2
claim_pendingmarker-only — still fully_addressed (re-runssession_is_crashed). - P2 Windows
Unsupported— still fully_addressed. - P1 heartbeat
elapsed().ok()fail-closed — still fully_addressed. - P2 beat quiet during
run_recovery— still fully_addressed. - P1/P2 vanished lock skips beat — still fully_addressed (
Probe::Vanished). - P0 duplicate
[target.'cfg(unix)'.dependencies]— still fully_addressed. One required unixlibc = "0.2";telemetry = ["dep:sysinfo"].
Findings
No remaining P0–P3 with a credible failure scenario. The empty remarge of main’s #24 squash does not reopen the live-session or double-delivery holes: extra-dir recovery still passes beat_marker, distinct_extra_dirs still avoids recovering one tree twice, and try_claim_lenient is still the drain/recovery degrade for a directory that cannot take a lock file.
CI
- 38579223977 (fbbabb2, same tree): all 8 jobs SUCCESS (ubuntu/macos/windows tests, MSRV, Android lint, both iOS lints, macOS private-API).
- 38580893770 (deead02): ubuntu/windows tests, MSRV, Android lint, both iOS lints, and the macOS private-API check SUCCESS; macos-latest tests still running at review time.
Overall risk: Low
Merge recommendation: Already merged — would have approved
Nothing blocking on this SHA. The #14 lock design is intact and the stack merge is a no-op tree.
Most important to fix
Nothing on this SHA.
What still looks solid
Kernel locks as the pid-namespace arbiter (fcntl not flock, so a prefork child does not inherit the parent’s claim); registry-before-open so POSIX cannot drop a held lock on a second close; Probe::Vanished vs Free; heartbeat that fails open on future mtimes (180s vs NFS acregmax) and is touched during launch recovery; O_RDONLY probe → Unknown; claim_pending re-running session_is_crashed; Windows Unsupported; keep_lock_until_exit; 60s queue-orphan grace; real child-process tests for racing launchers, SIGKILL, colliding marker pids, and a lost lock-race launch.
Residual (unchanged, accepted): ensure_marker still restores .alive without recreating .beat after a wiped tree; a single recover_one / blocking before_send longer than three minutes would not refresh the beat; queue/empty-marker graces still assume clocks within seconds. Delivery stays at-least-once if a holder dies between upload and removal.
Sent by Cursor Automation: Bugsee code review


Stacked on #24 (issue #9): the base is
fix/issue-9, because this touches the same storage and recovery code.Problem
Several processes sharing one data directory (prefork workers, a CLI beside a daemon, containers on one volume) could deliver a pending report twice, or lose one.
Root causes found by racing real processes
queue::gc_orphansat one process's launch reaped another process's in-flightenqueue(sidecars written, bundle not yet renamed), stranding the bundle without its.reqand losing the report. It now reaps only files untouched for 60 s.Design: OS file locks, nothing to go stale
Claims are byte-range locks on
*.lockfiles:fcntl(F_SETLK)on unix (per process, released when the process dies, not inherited byfork) andLockFileExon Windows.queue/<bundle>.lock: a queued report being delivered.sessions/<gen>.recover.lock: a crashed session being recovered.sessions/<gen>.lock: held by a live session for its whole life, so peers (including from another pid namespace) can tell live from crashed without trusting the marker's pid.There is no owner pid, start time, lease or takeover protocol. An in-process registry excludes threads and guarantees a process never opens one lock file twice (POSIX would drop the lock on the second close). It is canonicalised, spin-locked, and reset in the fork child. Details and trade-offs are in DESIGN.md section 8.
Other behaviour:
<gen>.lockremoves the file rather than leaving an unheld one, which peers would read as "that session died".F_GETLKand never takes the lock.ENOLCK,EOPNOTSUPP,ENOSYS,EINVAL) degrade to thread-only claims with a one-time stderr message; recovery falls back to the marker pid. The queue never blocks..alivemarker is still a bare pid and the lock files are new names, so older builds sharing a directory keep working. There is no exactly-once between old and new processes.origin/fix/issue-9: kept itstry_claim_lenient(a directory that cannot take a lock file proceeds unclaimed) on top of the lock-based claims.Review notes
Two independent review rounds changed the design: pid or start-time schemes were unsound across pid namespaces, and the first lock version had a launch race and no fallback for lock-less filesystems. Both are fixed and covered by tests.
Tests
crates/bugsee-core/tests/multiprocess.rsspawns real child processes: racing launchers over 8 rounds (each pending report delivered exactly once), a SIGKILLed lock holder not blocking others, a live peer whose marker names a colliding or nonexistent pid not being recovered (and recovered exactly once after it dies), and a launch that loses the lock race.cargo fmt --check,clippy -D warningsand the workspace tests pass on macOS with the known-hangingforking_while_another_thread_is_capturing_never_deadlocks_the_childskipped;multiprocess.rswas run 5 times in a row.Not verified
claim.rsandprocess.rstype-check forx86_64-pc-windows-msvc, but nothing was built or run there, including the multi-process tests./proczombie check andfcntlbehaviour on real Linux and NFS were not.nolock) cannot be detected; run one SDK host per directory there (documented).Closes #14
🤖 Generated with Claude Code