Repository navigation
feat: bounded launch-path recovery so crash loops can't outrun the report (#10) - #33
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)
…returns A host that crashes again right after launch (crash loop) could die before the worker thread recovered the previous crash. launch now runs a bounded, newest-first recovery pass (8 sessions, 500 ms budget via LaunchOptions::launch_recovery_budget, 4096-entry scan cap) that enqueues the reports durably before returning; upload stays async and the worker's pass picks up the rest, skipping what the launch pass attempted. Closes #10 🤖 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)
…ash handlers Address review of the launch-path recovery pass: - the pass runs on a helper thread and launch waits at most the budget, so a blocked before_send cannot stall or deadlock launch; the worker holds its own recovery until the pass is done (bounded) and skips what the pass claimed - Recorder::recover_on_launch_path is called by launch_with after the native handler and panic hook are installed, outside all locks - dedup repeated identical recovered crashes against the queue and cap it - recovered crashes carry no scope on either path; claimed-only attempted set; scan cap counts session markers only; docs, latency numbers, deterministic tests, facade test 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- 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 review: bounded launch-path recovery (#10)
Stacked on #32. Intent is right: a supervisor crash loop can die before the worker recovers the previous session, so the newest crashed sessions must become durable queue entries before
launchreturns. Handler install before the pass, helper-thread budget (no pre-fork parker),before_sendthreading docs, and the abort-after-launch subprocess test all match that contract.The same-signature crash loop is implemented and tested. Two holes still lose unique evidence the worker is documented to keep.
CI 38555735857: ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88 all SUCCESS. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Findings
P1 —
Admission::Fulldeletes unique crash evidence; the worker is told to skip those generations
recover_onetreatsFulllikeDuplicateand callsrecovery::discard.recover_claimedrecordslaunch_attemptedat claim time, before enqueue.run_recovery(..., skip_launch_pass=true)then leaves those gens alone. DESIGN.md §8 says the worker pass does not cap and that whatever the launch pass leaves is not lost. The function comment onrecover_onesays the same: discard only once queued.
admit_recoveredcounts every pending bundle (errors, bugs, crashes) toward 64 reports / 64 MiB. Duplicate-collapse of the same crash is fine; destroying a unique newest crash because the queue is already busy is not.P2 —
scan_capis applied inreaddirorder, before newest-first sort
find_pending_boundedtakesscan_cap*4dirents, countsscan_cap.alivefiles (including live peers and the current gen), then sorts. A cut-off scan can omit the newest marker. The documented mitigation is the worker’s unbounded pass, which a crash loop never reaches. Walkcurrent_generation-1downward instead; that is O(limit) probes and always hits the newest.P2 — newest-first plus a fault during recovery defeats
MAX_RECOVERY_ATTEMPTS
bump_attemptsruns beforerecover_one, but a native fault /abortin the helper (not a panic; those are caught) is recorded as this session. The next launch attempts that newer gen first. The original pending stays at attempts=1 and is never retried on the launch path, and the worker still has not run. Poison sessions accumulate instead of being abandoned after 3 launches.Overall risk: High
Unique recovered crashes can be deleted when the outbound queue is at cap, including a cap filled by unrelated undelivered reports. That is data loss in the exact subsystem this PR exists to protect.
Merge recommendation: request changes
Fix the Full path so unique evidence stays on disk (and is not claimed-as-done for the worker). The scan order and attempt-count starvation should be fixed on the same pass; they are the leftover ways a loop still outruns the report.
Most important to fix
- Do not
discardonAdmission::Full. Recordlaunch_attemptedonly after the session is queued, duplicate-dropped, host-dropped, or abandoned by attempts.- Find launch-path candidates by walking generation numbers down from
current-1, not by a capped unsortedreaddir.- Keep
MAX_RECOVERY_ATTEMPTSprogressing when recovery itself kills the process (include already-attempted gens in the bounded set, not only the newest).What looks solid
Helper thread + 1 ms poll so a blocking
before_sendcannot deadlock or stalllaunch; pass runs after the native handler and panic hook, outside the facade lock; claim/bump_attemptsbefore assemble; real crash-loop test (abortafterrecover_on_launch_path); zero-budget leaves work on the worker; latency numbers and the threading contract onLaunchOptions::before_sendare honest.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)
…est-first without a cap; keep attempts progressing - Admission::Full no longer discards the session; it is left to the worker - find_pending_bounded reads all marker names, sorts, then probes (no capped readdir), and includes sessions with prior attempts so a fault during recovery still reaches MAX_RECOVERY_ATTEMPTS 🤖 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
Re-review at 6d6faa2 (
6dfc9c8+ merge of #32)The round-1 P1 and the scan-order P2 are fixed.
Admission::Fullnow leaves evidence on disk and drops the gen fromlaunch_attemptedso the worker can queue it;find_pending_boundedreads every.alivename, sorts newest-first, then probes (and the unit test now asserts gens 9 and 7). Those match the stated contract.Two holes remain, both still unique-crash data loss on the launch path a crash loop actually takes (the worker never runs).
CI 38558202388: ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88 all SUCCESS. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 6d6faa2 P1 Admission::Fulldiscarded unique evidence; worker skipped vialaunch_attemptedfully addressed — no discard, gen removed fromlaunch_attempted;a_full_queue_defers_a_unique_crash_to_the_worker_instead_of_discarding_itcovers the process-lives pathP2 scan_capinreaddirorder before sortfully addressed P2 newest-first starves MAX_RECOVERY_ATTEMPTSpartially addressed — retries are in the scan set, but the pass still dies on the newest before bumping them Remaining findings
P2 —
Admission::Fullstill counts as a recovery attempt, so a crash loop with a full queue deletes unique evidence after 4 launches
bump_attemptsruns at claim, beforeadmit_recovered.Fullun-claims for the worker and returns withoutdiscard, but the sidecar already went 1→2→3. On the 4th launchattempts > MAX_RECOVERY_ATTEMPTS(3) discards the session that was never queued.
a_full_queue_defers_…does not abort, so the worker saves it. #10’s loop (abortright afterrecover_on_launch_pathreturns) never lets the worker run. Upload already failing with 64 reports (or ~64 MiB) inqueue/is the explicit cap this PR adds; combined with a supervisor restart loop it now destroys the newest unique crash instead of keeping it for a later healthy drain.With the default 500 ms budget a Full decision is cheap after
build_report, so the pass walks the whole bounded set and every unique pending gen burns an attempt each loop.P2 — including
.attemptsgens in the scan set does not makeMAX_RECOVERY_ATTEMPTSprogress
find_pending_boundedcan return[newest, …, original], anda_session_with_failed_attempts_is_not_starved_by_newer_onesasserts that.run_launch_recoverystill processes that vec newest-first. A native fault /abortinrecover_oneof the newest (hostbefore_send, or assemble) is recorded as this session; the original stays at attempts=1. Next launch the recovery crash is the new newest. The original is in the list and never reached.find_pendingnow shares that newest-first order, so the worker path has the same starvation if it is the one that faults.#10 wanted a crash during recovery to be recoverable; handlers do that. Prioritizing that new session forever still prevents abandoning the poison original after 3 launches, and still prevents queueing it.
Overall risk: Medium
The process-lives Full path and the newest-first scan are correct. Remaining loss is crash-loop only: unique evidence vs a full outbound queue, and the original session when recovery itself keeps dying.
Merge recommendation: request changes
Most important to fix
- Do not count
Fullas a recovery attempt (restore the previous.attemptsvalue, or bump only after Admit / intentional drop / assemble). Leave a capped-out unique session untouched until the queue can take it.- Make already-attempted gens actually run (or at least
bump_attempts) before recovering the newest, so a fault in the newest still drives the original toMAX_RECOVERY_ATTEMPTS— and so a good original can be queued before the recovery-crash is attempted.What looks solid
Helper thread + 1 ms poll; pass after native handler and panic hook, outside the facade lock; Full no longer deletes; scan names then sorts; crash-loop subprocess test; Full→worker test;
before_sendthreading docs; CI green on this head.Sent by Cursor Automation: Bugsee code review
…tempt - find_pending_bounded returns sessions with prior attempts before newer ones, so MAX_RECOVERY_ATTEMPTS progresses even if recovering the newest keeps faulting - Admission::Full gives the attempt back (undo_attempt) - make the full-queue test race-free 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- 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
Re-review at 4530024
Both remaining P2s from 6d6faa2 are fixed.
Admission::Fullis not an attempt.undo_attemptrestores the sidecar (or removes it when the count would return to 0), so a crash loop against a full queue can no longer tick 1→2→3 anddiscardunique evidence. Unit testan_undone_attempt_is_not_countedcovers the counter; the process-lives Full path still lands on the worker.- Already-tried sessions run first.
find_pending_boundedreturns.attemptsgens before fresh newest-first ([2, 6, 5]). A native fault/abortwhile recovering the newest still leaves the original at the head of the next launch’s list, so it can be queued or reachMAX_RECOVERY_ATTEMPTS. The worker uses the same order viafind_pending.I did not find a remaining P0–P3 with a credible failure scenario. Residual: the launch pass may spend its budget on retries before the newest fresh session; that is the tradeoff that makes the poison-pill cap work, and a same-signature crash loop still has no retries (previous session was discarded).
CI 38559675665: ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88 all SUCCESS. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 4530024 P1 Fulldiscarded unique evidencefully addressed (earlier) P2 scan-cap before newest-first sort fully addressed (earlier) P2 FullburnedMAX_RECOVERY_ATTEMPTSfully addressed ( undo_attempt)P2 newest-first starved retries fully addressed (retries first) Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Helper-thread budget, handler install before the pass, Duplicate vs Full split, name-then-sort scan, crash-loop subprocess test, and now retries-first plus attempt undo — the launch-path guarantee holds for the supervisor loop, a full outbound queue, and a fault during recovery.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 4530024
Both leftover P2s from 6d6faa2 are fixed.
Admission::Fullnow gives the attempt back (undo_attempt), andfind_pending_boundedreturns already-tried sessions first (test:[2, 6, 5]), so a fault while recovering the newest still drives the original toMAX_RECOVERY_ATTEMPTS(or queues it). Those match what was asked.One sibling hole remains: the other launch-path refusal that says nothing about the evidence —
queue::enqueuefailing — still burns the attempt and still marks the gen handled for the worker.CI 38559675665: ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88 all SUCCESS. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 4530024 P1 Admission::Fulldiscarded unique evidence; worker skipped vialaunch_attemptedfully addressed P2 scan_capinreaddirorder before sortfully addressed P2 newest-first starves MAX_RECOVERY_ATTEMPTSfully addressed — retries are processed first P2 Fullburned attempts so a crash loop deleted a unique crash on launch 4fully addressed — undo_attemptRemaining finding
P2 —
enqueuefailure afterAdmitstill counts as a recovery attempt, so a crash loop on an almost-full disk deletes unique evidence after 4 launches
admit_recoveredreturnsFullonly when 64 reports / 64 MiB are already queued. A volume that is almost full typically has few queued bundles (the writes were already failing), so the new crash isAdmitted.recover_claimedhas bumped.attempts(a few bytes undersessions/— that write fits).assemble_with_extrasbuilds the zip in memory.queue::enqueuethen fails on the bundle. TheFullarm is not taken: noundo_attempt, gen stays inlaunch_attempted.#10’s loop (
abortright afterrecover_on_launch_path) never lets the worker run and throws awayhold_in_memory. Launch 4 hitsattempts > 3anddiscards a session that was never queued. That is the same data-loss shape as the last P2, on the path #9/#24 exists for (ENOSPC: sidecars succeed, the zip does not).A failed
assemble(corrupt parts) should still count. A failed queue write should not.Overall risk: Medium
The process-lives Full path, tried-first retries, and newest-first scan are correct. Remaining loss is crash-loop + almost-full disk only.
Merge recommendation: request changes
Most important to fix
- Treat
queue::enqueuefailure likeAdmission::Full:undo_attempt, and drop the gen fromlaunch_attemptedso a living process’s worker can retry. Leaveassembleerrors counting toward the poison-pill cap.What looks solid
Helper thread + 1 ms poll; pass after native handler and panic hook, outside the facade lock; Full no longer deletes or burns attempts; retries processed first; crash-loop subprocess test; Full→worker test;
before_sendthreading docs; CI green on this head.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 4002558
The leftover P2 is fixed. On
queue::enqueuefailure,recover_onenowundo_attempts (same class of refusal asAdmission::Full). A crash loop on an almost-full volume can no longer tick.attemptsto 4 anddiscarda unique crash that was never queued.assembleerrors still count as poison. The launch pass keeps the gen inlaunch_attemptedso a living process does nothold_in_memorytwice — that is the right split: the crash-loop hole was the attempt counter, not the worker skip.No remaining P0–P3 with a credible failure scenario.
CI 38561613301: MSRV 1.88 and Android lint already SUCCESS; ubuntu/macOS/Windows test+lint and iOS were still running when this review was written. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 4002558 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Fullburned attemptsfully addressed P2 enqueuefailure burned attemptsfully addressed ( undo_attempt; claim kept so the worker does not double-hold)Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Launch-path recovery now treats both queue-cap and disk-write refusal as non-attempts, processes already-tried sessions first, and still bounds
launchwith the helper-thread budget after the crash handlers are installed.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at b606938
The leftover P2 is fixed.
Admission::Fullnow setslaunch_deferred. Afterlaunch_done,worker_loopswaps that flag and runsrun_recoveryagain (claims already released;launch_attemptedno longer contains the Full gen). Abefore_sendthat outrunsbudget + 5scan no longer strand a unique crash for the process lifetime: capture stays unblocked, and the session is picked up on the next worker tick (≤HEARTBEAT_EVERY, 2s).If the helper finishes inside the wait, the first worker pass already recovers it; the deferred pass is a no-op (
find_pendingempty). Multiple Fulls share one flag and one rescan.No remaining P0–P3 with a credible failure scenario. Residual: no test drives slow
before_send+ Full past the wait (the existing full-queue test still only covers the fast path).CI 38563683907: ubuntu/macOS test+lint, Android, iOS, MSRV 1.88 SUCCESS; Windows still pending. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At b606938 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Fullburned attemptsfully addressed P2 enqueuefailure burned attemptsfully addressed P2 Full→worker skipped if helper outran the wait fully addressed ( launch_deferred+ rescan inworker_loop)Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Launch-path recovery now treats queue-cap and disk-write refusal as non-attempts, processes retries first, and still hands a deferred unique crash to the worker after a slow
before_send.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at b606938
launch_deferredplus a follow-uprun_recoveryinworker_loopfixes the previous P2 on the writable-disk path:launch_doneis stored only after the helper drops its claims, so the worker can pick up a unique crash thatAdmission::Fullreleased after the budget+5s wait. Prior launch-path P1/P2s still hold.The follow-up pass is aimed at the wrong directory.
worker_loop'sdata_diris the live capture tree (store.is_some(),Nonein memory/degraded mode). Startup recovery already usesdrain_dirfor exactly that case.swap(false)also consumes the flag when theif let Some(dir)fails, so nothing retries after the queue later drains.CI 38563683907 SUCCESS (ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88). I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At b606938 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Fullburned attemptsfully addressed P2 enqueuefailure burned attemptsfully addressed P2 Full→worker handoff was one-shot if before_sendoutran the waitfully addressed on a writable disk; residual below when the directory cannot take a new session Remaining finding
P2 — deferred recovery uses the capture dir, so a unique
Fullcrash is stranded for the life of a memory-mode processSee the inline comment. #9/#24 already run in memory mode against an existing recovery tree (read-only remount, ENOSPC on
Session::begin) while still drainingqueue/and recovering sessions. That is the situation where the outbound queue is most likely already at 64.Admission::Fulldoes nothold_in_memory; it only setslaunch_deferred. The new pickup then no-ops.Overall risk: Medium
Crash-loop evidence is still kept (
undo_attempt). A living process with a writable session dir now queues the deferred crash. The miss is the degraded directory the rest of this stack exists to handle: unique crash stays on disk after the queue drains, until the next restart.Merge recommendation: request changes
Most important to fix
- Run the deferred pass against
drain_dir(the same path as the worker's firstrun_recovery), and do notswap(false)when that path is missing. Cover it with a test: full queue,before_sendlonger thanbudget + 5s, andopen_diskfailing /memory_onlywith the directory still present.What looks solid
Gating the follow-up on
launch_done(set after claims drop) is the right happens-before for the disk path. Retries-first,undo_attempton both queue-cap and enqueue-write refusal, Duplicate vs Full, name-then-sort scan, abort-after-launch crash-loop test, and handler install before the helper are all still in good shape.Sent by Cursor Automation: Bugsee code review
… not only the capture tree 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Re-review at 12d4672
The leftover P2 is fixed. Deferred recovery now uses
recover_dir(drain_dir/ the same tree as the worker's firstrun_recovery), not the live capturedata_dir.launch_deferredis onlyswapped when that directory exists, so memory/degraded mode (#9/#24: no new session, existing tree still drained) no longer eats the flag and no-ops.No remaining P0–P3 with a credible failure scenario. Residual: still no test for Full +
before_sendpast the wait + unusable-but-present data dir.CI 38563683907 successor on this head: MSRV 1.88, Android, iOS lint SUCCESS; ubuntu/macOS/Windows test+lint still running. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 12d4672 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Full/enqueueburned attemptsfully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture dir (None in memory mode) fully addressed ( recover_dir= drain tree; swap only whenSome)Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 12d4672
The leftover P2 is fixed. Deferred recovery now uses
recover_dir(drain_dir, the same tree as the worker's firstrun_recovery), not the live capturedata_dir.launch_deferred.swap(false)sits insideif let Some(dir), so a missing path no longer eats the flag. In #9/#24 memory mode the existing recovery tree is still walked after a slowbefore_send+Admission::Full; the worker path does not cap, so a unique crash is enqueued orhold_in_memory'd.No remaining P0–P3 with a credible failure scenario. Residual: still no test for helper-outliving-the-wait on a drain-only directory.
CI 38565086668: MSRV 1.88, Android, iOS SUCCESS; ubuntu/macOS/Windows tests still running. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 12d4672 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Full/enqueueburned attemptsfully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture data_dir(Nonein memory mode)fully addressed ( recover_dir=drain_dir; swap only whenSome)Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Launch-path recovery now treats queue-cap and disk-write refusal as non-attempts, processes retries first, hands a deferred unique crash to the worker after a slow
before_send, and does that on the drain tree in memory mode too.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 12d4672
The leftover P2 is fixed. Deferred recovery now uses
worker_drain_dir(the same tree as the worker's firstrun_recovery), andlaunch_deferred.swap(false)only runs when that path isSome. In memory/degraded mode (#9/#24: directory exists,open_diskfailed)data_diris stillNone, but the follow-up pass no longer no-ops, and it no longer eats the flag when there is nothing to recover.
launch_doneis still stored only after the helper drops its claims, so a uniqueAdmission::Fullcrash that outlivedbudget + 5sis picked up on the next worker tick (≤HEARTBEAT_EVERY, 2s). If the helper finished inside the wait, the first worker pass already queued it and the deferred pass is a no-op.No remaining P0–P3 with a credible failure scenario. Residual: still no test that drives Full +
before_sendpast the grace + an unusable-but-present session dir (the existinga_full_queue_defers_…case is a writable dir and a helper that finishes inside the wait).CI 38565086668 SUCCESS (ubuntu / macOS / Windows test+lint, Android, iOS, MSRV 1.88). I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 12d4672 P1 Fulldiscarded unique evidencefully addressed P2 scan_capbefore newest-first sortfully addressed P2 newest-first starved retries fully addressed P2 Fullburned attemptsfully addressed P2 enqueuefailure burned attemptsfully addressed P2 Full→worker skipped if helper outran the wait fully addressed P2 deferred recovery used the capture dir ( Nonein memory mode)fully addressed ( recover_dir=drain_dir; swap gated onSome)Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Launch-path recovery now treats queue-cap and disk-write refusal as non-attempts, processes retries first, hands a deferred unique crash to the worker after a slow
before_send, and does that against the recovery tree even when this process cannot take a new session.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)
There was a problem hiding this comment.
Stale comment
Re-review at bb281f1
Merge of #9 (
fix/issue-9) plus a 60s observable wait ina_full_queue_defers_…. This PR's launch-path machinery is intact:undo_attempton Full and enqueue failure, retries first,launch_deferred+recover_dir(drain tree, swap only whenSome).distinct_extra_dirsis a help here: recovering the same tree twice in one launch would double-bumpMAX_RECOVERY_ATTEMPTS. Recovered crashes now honour the in-memory blacklist as well as the on-disk one (intentional drop, same asany_blacklisted).No remaining P0–P3 on the launch-path work.
Overall risk: Low
Merge recommendation: approve
Stacked on #32 and #24; retarget to main after those merge.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at bb281f1
Two changes on top of 12d4672: a merge of latest #9/#24 (
distinct_extra_dirs,retire_queuedon every terminalqueue::remove, mem-blacklist consult inrecover_one), and the full-queue test now waits up to 60s on an observable marker rather than a 10s race against helper+grace.Launch-path recovery is unchanged in contract. The #9 merge does not reopen Full/undo/deferred-handoff: extras are still worker-only, and
recover_diris stilldrain_dir.mem_blacklistedon recovered crashes matches the worker; the disk.blacklistcheck was already there.No remaining P0–P3 with a credible failure scenario. Residual: still no test for Full +
before_sendpast the wait on a drain-only directory (the full-queue test is a writable dir and a helper that is allowed to finish inside the wait).I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At bb281f1 P1 Fulldiscarded unique evidencefully addressed P2 scan-cap / retries-first / attempt undo fully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture dir fully addressed Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at bb281f1
New since the last approve (
12d4672): merge of #9/#24 (fbd6644) and a full-queue test that waits onlist_pendinglength with a 60 s bound instead of 10 s.The #9 merge does not reopen the launch-path holes.
recover_diris still the drain tree (worker_drain_dir), so memory/degraded mode still gets the deferred Full pickup.mem_blacklistedonrecover_oneis the same any-match the worker already applied via the on-disk blacklist; extras/legacy dirs stay worker-only, as DESIGN.md §8 says.distinct_extra_dirs/retire_queuedare #9 correctness, not a new #10 interaction.The test change is a flake bound, not a product fix: helper + worker wait is
budget + 5 s, then at mostHEARTBEAT_EVERY. 60 s with the marker in the timeout message is the right assertion for a loaded CI machine.I re-checked the previous failure scenarios against this tree (Full discard, scan-before-sort, retries starved, Full/enqueue burning attempts, one-shot Full→worker, deferred pass on
data_dir == None). They still do not reproduce.No remaining P0–P3 with a credible failure scenario. Residual: still no test that drives Full +
before_sendpast the grace on a drain-only directory (the existing full-queue case is a writable dir and a helper that finishes inside the wait).CI 38566562287 was still running on this head. Last green: 38565086668 at 12d4672. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At bb281f1 P1 Fulldiscarded unique evidencefully addressed P2 scan_capbefore newest-first sortfully addressed P2 newest-first starved retries fully addressed P2 Fullburned attemptsfully addressed P2 enqueuefailure burned attemptsfully addressed P2 Full→worker skipped if helper outran the wait fully addressed P2 deferred recovery used the capture dir ( Nonein memory mode)fully addressed Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Launch-path recovery still treats queue-cap and disk-write refusal as non-attempts, processes retries first, hands a deferred unique crash to the worker after a slow
before_send(including on the drain tree in memory mode), and the abort-after-launch subprocess test still matches #10.Sent by Cursor Automation: Bugsee code review
🤖 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
Re-review at e00b09d
Stack merge of latest #14 (which merged #9, which merged
main). Launch-path files (runtime.rs,recovery.rs,queue.rs,launch_recoverytests) are unchanged from bb281f1. The only facade delta isBugsee::uploadbuildingmanual_upload_meta()off the global lock (#25);recover_on_launch_pathstill runs last, afterbugsee_native::installand the panic hook.Prior launch-path P1/P2s still hold. No remaining P0–P3 with a credible failure scenario. Residual unchanged: no Full + slow
before_send+ drain-only-dir test.I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).
Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at e00b09d
This synchronize is a merge of latest #14 (
e01a20a, which itself merged #9 andmain, including #23 CrashBackend and #27 hostinfo) intofix/issue-10. Launch-path sources (runtime.rs,recovery.rs,queue.rs,launch_recoverytests) are byte-identical to bb281f1.recover_on_launch_pathstill runs last inlaunch_with, afterbugsee_native::install(now aCrashBackendwrapper) and the panic hook. The only facade delta on this merge isBugsee::uploadbuildingmanual_upload_meta()off the global lock (#25).Prior launch-path P1/P2s still hold. The new hole is not in #10 logic: the merge imported a broken
crates/bugsee/Cargo.tomlfrom #32/#14, and this branch no longer loads.CI 38577801406: ubuntu / Windows / MSRV 1.88 / Android / iOS-sim all FAILURE at
cargo metadata(~13–44s). Last green on this branch: 38566562287 at bb281f1. I did not run tests locally (agent rustc 1.83, crate MSRV 1.88).Findings
P0 — duplicate
[target.'cfg(unix)'.dependencies]incrates/bugsee/Cargo.tomlmakes the workspace unloadable
- Location:
crates/bugsee/Cargo.toml:20(libc = "0.2") and:48(libc = { version = "0.2", optional = true }plustelemetry = ["dep:sysinfo", "dep:libc"]at:35)- Problem: Cargo forbids two tables with the same name.
e00b09dkept both: the always-on unixlibcthis crate already needed (options.rsgeteuid, fork tests) and the second table #27 added for hostinfo.- Impact:
cargo metadatafails; every CI job that parses the workspace dies before compile. The branch cannot be built, tested, or merged. bb281f1 was green; this merge is what made it red. Same duplicate is on #32 (fix/issue-14) — inherited, not unique #10 code — but it is now this PR's HEAD.- Scenario:
git merge origin/fix/issue-14did not conflict (the tables sit at different lines).cargo check/ CI then:error: duplicate keyat line 48,failed to load manifest for workspace member crates/bugsee.- Fix: one unix-dependencies table. Keep non-optional
libc = "0.2"(data-dir uid and tests need it with telemetry off). Drop the second table and restoretelemetry = ["dep:sysinfo"].hostinfo.rsis alreadycfg(feature = "telemetry")and can use the always-on unix dep. Prefer landing that on #32 and remarging; collapsing it here also unblocks this PR.Prior findings
Finding At e00b09d P1 Fulldiscarded unique evidencefully addressed P2 scan_capbefore newest-first sortfully addressed P2 newest-first starved retries fully addressed P2 Full/enqueueburned attemptsfully addressed P2 Full o worker skipped if helper outran the wait fully addressed P2 deferred recovery used the capture dir ( Nonein memory mode)fully addressed Overall risk: High
Launch-path recovery is unchanged and still correct. The merge made the crate unloadable, so the #10 guarantee cannot be exercised on this head.
Merge recommendation: request changes
Do not merge until
cargo metadatasucceeds. Unique #10 work does not need another pass after the manifest is collapsed.Most important to fix
- Collapse the two
[target.'cfg(unix)'.dependencies]tables (on #32, or here) so CI is green again.What looks solid
Helper-thread budget after native handler and panic hook, retries-first plus
undo_attempton queue-cap and enqueue-write refusal,launch_deferredon the drain tree, abort-after-launch crash-loop test. CrashBackendinstall()is still the same entrylaunch_withuses.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at e00b09d
Merge of
origin/fix/issue-14(e01a20a: #14 + #9 + main #21–#29) onto last-reviewedbb281f1. Unique launch-path files (runtime.rs,recovery.rs,queue.rs, tests) are unchanged. Incoming origin/CrashBackend/hostinfo do not reopen the #10 holes: recovered reports still setsource.origin: None, andlaunch_withstill installs the native handler then the panic hook beforerecover_on_launch_path.The remarge left the workspace unloadable.
P0 — duplicate
[target.'cfg(unix)'.dependencies]tables, so cargo never starts
crates/bugsee/Cargo.tomlhas both:
- line 20:
libc = "0.2"(required; this crate always callslibc::geteuidfromoptions.rsfor the private temp dir)- line 48:
libc = { version = "0.2", optional = true }plustelemetry = ["dep:sysinfo", "dep:libc"]from #27TOML forbids two
dependencieskeys on the same target table. Confirmed locally:error: invalid table header duplicate key `dependencies` in table `target.cfg(unix)` --> crates/bugsee/Cargo.toml:48:1
cargo metadataandcargo fmt --all --checkfail before any compile. CI 38577801406: MSRV build, Ubuntu format, Windows build, Android clippy all fail in seconds. The same duplicate is on basee01a20a; this remarge imported it (it is not in the three-dot unique diff).Fix: one unix table, required
libc = "0.2". Dropdep:libcfromtelemetry— the comment on the second table already says libc is in the tree. Optional-only libc would breakoptions.rswhentelemetryis off.Prior findings
Finding At e00b09d P1 Fulldiscarded unique evidencefully addressed P2 scan-cap before newest-first sort fully addressed P2 newest-first starved retries fully addressed P2 Full/enqueueburned attemptsfully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture data_dirfully addressed Overall risk: High
Unique #10 recovery is still Low. HEAD cannot be built or tested.
Merge recommendation: request changes
Most important to fix
- Collapse the two unix
libctables so cargo loads. Unique launch-path code needs no further change.What looks solid
Helper-thread budget after the native handler and panic hook, retries-first plus
undo_attempton queue-cap and enqueue-write refusal, Duplicate vs Full, name-then-sort scan,launch_deferredon the drain tree, abort-after-launch crash-loop test. Residual: still no Full + slowbefore_send+ drain-only-dir test.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)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Re-review at 66e4f18
The P0 is fixed. One
[target.'cfg(unix)'.dependencies]table, requiredlibc = "0.2", andtelemetry = ["dep:sysinfo"](nodep:libc). I confirmedcargo metadata --offline --no-depssucceeds on this head.--no-default-featuresstill gets unixlibcforoptions.rsgeteuid. Launch-path sources are unchanged from bb281f1 / e00b09d.No remaining P0–P3 with a credible failure scenario. Residual: still no Full + slow
before_send+ drain-only-dir test.I did not run the test suite locally (agent rustc 1.83, crate MSRV 1.88).
Prior findings
Finding At 66e4f18 P0 duplicate unix libctablesfully addressed P1 Fulldiscarded unique evidencefully addressed P2 scan-cap / retries-first / attempt undo fully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture dir fully addressed Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 66e4f18
Remarge of #14 after
1ecfca8(de-duplicate unixlibc). Unique launch-path files are unchanged from e00b09d / bb281f1. The only delta iscrates/bugsee/Cargo.toml: one[target.'cfg(unix)'.dependencies]table with requiredlibc = "0.2", andtelemetry = ["dep:sysinfo"](nodep:libc).Confirmed locally:
cargo metadata --offline --no-depsexits 0.recover_on_launch_pathstill runs last inlaunch_with(afterbugsee_native::installand the panic hook).Prior findings
Finding At 66e4f18 P0 duplicate unix libctables, workspace unloadablefully addressed P1 Fulldiscarded unique evidencefully addressed P2 scan_cap/ retries-first / attempt undofully addressed P2 Full→worker one-shot after wait fully addressed P2 deferred pass used capture dir fully addressed No remaining P0–P3 with a credible failure scenario. Residual: still no Full + slow
before_send+ drain-only-dir test.CI 38579231486: MSRV 1.88 and Android lint already SUCCESS; ubuntu/macOS/Windows tests and iOS still running. I did not run
cargo testlocally (agent rustc 1.83, crate MSRV 1.88).Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Helper-thread budget after the native handler and panic hook, retries-first plus
undo_attempton queue-cap and enqueue-write refusal,launch_deferredon the drain tree, abort-after-launch crash-loop test, and the workspace loads again.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review at 66e4f18
Synchronize is a remarge of latest
#32(fbbabb2, which includes1ecfca8). Unique launch-path files (runtime.rs,recovery.rs,queue.rs,launch_recoverytests,options.rs) are unchanged from bb281f1.recover_on_launch_pathstill runs last inlaunch_with, afterbugsee_native::installand the panic hook, outside the facade lock. Recovered reports still setsource.origin: None.The previous P0 is fixed:
crates/bugsee/Cargo.tomlnow has a single[target.'cfg(unix)'.dependencies]table with requiredlibc = "0.2", andtelemetryisdep:sysinfoonly. Confirmed locally:cargo metadata --offline --no-depsexits 0 (it failed at e00b09d withduplicate key 'dependencies' in table 'target.cfg(unix)').I re-checked the prior failure scenarios against this tree (Full discard, scan-before-sort, retries starved, Full/enqueue burning attempts, one-shot Full→worker, deferred pass on
data_dir == None). They still do not reproduce.Admission::Fullstillundo_attempts, drops the gen fromlaunch_attempted, and setslaunch_deferred; enqueue-write refusal stillundo_attempts and keeps the claim; deferred pickup still usesrecover_dir(drain_dir) and onlyswaps when that path isSome.No remaining P0–P3 with a credible failure scenario. Residual unchanged: still no test for Full +
before_sendpastbudget + 5son a drain-only directory (the existing full-queue case is a writable dir and a helper that is allowed to finish inside the wait).CI 38579231486: MSRV 1.88, Ubuntu test+lint, Windows test+lint, Android lint SUCCESS. macOS test+lint, iOS lint, and the private-API job were still running when this review was written. I did not run the crate tests locally (agent rustc 1.83, crate MSRV 1.88);
cargo metadatawas run and succeeded.Prior findings
Finding At 66e4f18 P0 duplicate unix libctables (unloadable workspace)fully addressed ( 1ecfca8on#32, remarged)P1 Fulldiscarded unique evidencefully addressed P2 scan_capbefore newest-first sortfully addressed P2 newest-first starved retries fully addressed P2 Full/enqueueburned attemptsfully addressed P2 Full→worker skipped if helper outran the wait fully addressed P2 deferred recovery used the capture dir ( Nonein memory mode)fully addressed Overall risk: Low
Merge recommendation: approve
Stacked on #32; retarget to main after that merges.
What looks solid
Workspace loads again. Launch-path recovery still treats queue-cap and disk-write refusal as non-attempts, processes retries first, hands a deferred unique crash to the worker after a slow
before_send(including on the drain tree in memory mode), and the abort-after-launch subprocess test still matches #10.Sent by Cursor Automation: Bugsee code review
Tree unchanged: this branch already contains #14's content. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Re-review at e247036 (empty remarge of main’s #14 squash)
git diff 66e4f18..e247036 is empty. Merge message: “Tree unchanged: this branch already contains #14's content.” Unique #10 files are unchanged from the last approve (bb281f1 / 66e4f18). GitHub now shows this PR merged into main (6494a4fa).
Re-read the launch-path machinery against callers, worker handoff, and tests — not just the remarge.
What I re-checked
Bugsee::launch_withstill runsrecover_on_launch_pathlast, after the native handler and panic hook, outside every lock.- Helper thread + budget poll (no park); panics contained;
launch_doneset even if spawn fails. find_pending_bounded: full name scan, sort newest-first, retries (.attempts) processed first, probe caplimit * 16.Admission::Full/ enqueue-write failure:undo_attempt, evidence kept; Full also clearslaunch_attemptedand setslaunch_deferred.- Worker wait is one-shot (
budget + 5s);worker_loopthen runs a secondrun_recoveryonrecover_dir(drain_dir) afterlaunch_done, so a Full that outlives the wait is not stranded — including memory mode with a still-present recovery tree. - Claims (#14) keep the helper and worker exclusive; recovered crashes still take no scope from the new process.
crates/bugsee/Cargo.toml: one unixlibc = "0.2"table (plus unixdev-dependencies).cargo metadata --offline --no-depsexits 0.
Prior findings
| Finding | At e247036 |
|---|---|
P0 duplicate unix libc tables |
fully_addressed |
P1 Full discarded unique evidence |
fully_addressed |
| P2 scan-cap / retries-first / attempt undo | fully_addressed |
| P2 Full → worker one-shot after wait | fully_addressed |
| P2 deferred pass used capture dir | fully_addressed |
No remaining P0–P3 with a credible failure scenario.
Residual (not blocking): there is still no test for Full + before_send longer than budget + 5s + drain-only / memory mode. The recover_dir path covers it in code; a_full_queue_defers_a_unique_crash_to_the_worker only hits the writable-disk, helper-finishes-inside-wait case.
CI
38581434695 on e247036: all 8 jobs SUCCESS (MSRV 1.88, Ubuntu, Windows, Android lint, macOS tests, macOS private-API, iOS device + sim). Local cargo test not run (agent rustc 1.83 vs MSRV 1.88).
Verdict
- Overall risk: Low
- Merge recommendation: Already merged; this SHA would have been approved.
- Most important issues to fix: None remaining.
- Positives: The #10 contract is intact after the #14 squash remarge: bounded launch-path queueing, retries-first so a recovery fault still reaches
MAX_RECOVERY_ATTEMPTS, and unique crashes the queue cap refuses stay on disk for the worker instead of being destroyed. Crash-loop test still covers abort-after-launch collapsing repeats to one queued report.
Sent by Cursor Automation: Bugsee code review


Summary
A host that crashes again within milliseconds of launch (systemd/Kubernetes restart loop, launch crash) could die before the worker thread recovered the previous crash, so the report was never produced. Recovery of the newest crashed sessions now finishes on the launch path as queue entries on disk; upload stays asynchronous.
Design
Bugsee::launch_withends withRecorder::recover_on_launch_path(), called after the native crash handler and panic hook are installed and outside every lock. A fault inside the pass is therefore recorded with a stack against the new session, and the previous session's evidence stays on disk until its report is queued (attempts are counted before processing, capped atMAX_RECOVERY_ATTEMPTS).bugsee-launch-recovery);launchwaits at most the budget by polling a flag (not parking: a parker used beforeforktraps in the child under libdispatch on macOS). That is a hard bound whateverbefore_senddoes, so a host holding its own lock cannot deadlock against the callback. On timeout the pass finishes in the background.LaunchOptions::launch_recovery_budget(default 500 ms;Duration::ZEROdisables the pass). CoreRecorderConfig::launch_recovery_budgetdefaults to zero; with a non-zero budget the worker holds its own recovery until the pass is done (budget plus 5 s grace) and skips the generations the pass claimed. Multiple processes sharing one data directory can double-deliver reports #14 claims keep the two exclusive in any interleaving.scan_capcounts session markers only; recovered crashes carry no scope from the new process on either path.before_send/launch_recovery_budgetrustdoc (threading contract).Launch latency (Apple silicon, synthetic log-only windows, default 8 MiB / 60 s caps)
The 8-session figure undercounts assembly: 7 are dropped as duplicates before being assembled. Scrubbing (#11) will add per-session cost. Reproduce with
cargo test -p bugsee-core --release --test launch_recovery -- --ignored --nocapture.Tests
bugsee-core/tests/launch_recovery.rs: previous crash queued when the launch path returns; time cap and count cap (deterministic); a blockedbefore_sendcannot stall launch; zero budget; a subprocess crash loop (childrenabort()right after launch, each relaunch already holds the first crash, repeats collapse, the healthy run delivers it exactly once). Unit tests forqueue::admit_recoveredand the bounded scan, and a facade test.Caveats
Stacked on #32 (#14) which is stacked on #24 (#9); retarget to main after those merge.
Closes #10. Delivers the launch-path guarantee (bounded, measured) and launch-pass dedup + a queue cap; dedup is a signature-set heuristic — if the maintainers want stricter dedup, that can be a follow-up issue.
🤖 Generated with Claude Code