Skip to content

feat: bounded launch-path recovery so crash loops can't outrun the report (#10) - #33

Merged
krassx merged 35 commits into
mainfrom
fix/issue-10
Oct 14, 2026
Merged

krassx merged 35 commits into
mainfrom
fix/issue-10

Conversation

@krassx

@krassx krassx commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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_with ends with Recorder::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 at MAX_RECOVERY_ATTEMPTS).
  • The pass runs on a helper thread (bugsee-launch-recovery); launch waits at most the budget by polling a flag (not parking: a parker used before fork traps in the child under libdispatch on macOS). That is a hard bound whatever before_send does, so a host holding its own lock cannot deadlock against the callback. On timeout the pass finishes in the background.
  • Knob: LaunchOptions::launch_recovery_budget (default 500 ms; Duration::ZERO disables the pass). Core RecorderConfig::launch_recovery_budget defaults 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.
  • Bounds: newest first, at most 8 sessions, no new session after the budget (the newest is always attempted), scan of at most 4096 session markers. Newest first because a crash loop makes only recent evidence reachable and the latest crash most likely belongs to the running build; the worker recovers whatever is left, so nothing is lost.
  • Crash-loop escalation: in the pass, a recovered crash whose signature set equals one already queued is dropped (the first copy is kept), and recovered crashes stop being queued at 64 reports or 64 MiB.
  • scan_cap counts session markers only; recovered crashes carry no scope from the new process on either path.
  • Docs: DESIGN.md section 8 and before_send / launch_recovery_budget rustdoc (threading contract).

Launch latency (Apple silicon, synthetic log-only windows, default 8 MiB / 60 s caps)

Case release debug
empty directory 0.1 to 0.3 ms similar
1 crashed session about 7 ms about 21 ms
8 identical sessions about 17 ms about 43 ms

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 blocked before_send cannot stall launch; zero budget; a subprocess crash loop (children abort() right after launch, each relaunch already holds the first crash, repeats collapse, the healthy run delivers it exactly once). Unit tests for queue::admit_recovered and the bounded scan, and a facade test.

Caveats

  • Dedup is a heuristic (identical signature set against what is currently queued); no repeat counter on the kept report. The worker pass does not dedup or cap.
  • Recovered crashes carry no scope from the new process.
  • A prefork worker boot loop (fork children) recovers on its worker thread, not on a launch path; legacy-dir sessions are worker-only.
  • Only macOS was run locally; Linux and Windows are covered by CI only.

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

krassx added 15 commits October 14, 2026 14:10
…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)
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)
- 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)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 launch returns. Handler install before the pass, helper-thread budget (no pre-fork parker), before_send threading 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::Full deletes unique crash evidence; the worker is told to skip those generations

recover_one treats Full like Duplicate and calls recovery::discard. recover_claimed records launch_attempted at 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 on recover_one says the same: discard only once queued.

admit_recovered counts 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_cap is applied in readdir order, before newest-first sort

find_pending_bounded takes scan_cap*4 dirents, counts scan_cap .alive files (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. Walk current_generation-1 downward instead; that is O(limit) probes and always hits the newest.

P2 — newest-first plus a fault during recovery defeats MAX_RECOVERY_ATTEMPTS

bump_attempts runs before recover_one, but a native fault / abort in 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

  1. Do not discard on Admission::Full. Record launch_attempted only after the session is queued, duplicate-dropped, host-dropped, or abandoned by attempts.
  2. Find launch-path candidates by walking generation numbers down from current-1, not by a capped unsorted readdir.
  3. Keep MAX_RECOVERY_ATTEMPTS progressing 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_send cannot deadlock or stall launch; pass runs after the native handler and panic hook, outside the facade lock; claim/bump_attempts before assemble; real crash-loop test (abort after recover_on_launch_path); zero-budget leaves work on the worker; latency numbers and the threading contract on LaunchOptions::before_send are honest.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread crates/bugsee-core/src/runtime.rs
Comment thread crates/bugsee-core/src/recovery.rs Outdated
Comment thread crates/bugsee-core/src/runtime.rs
… 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)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 6d6faa2 (6dfc9c8 + merge of #32)

The round-1 P1 and the scan-order P2 are fixed. Admission::Full now leaves evidence on disk and drops the gen from launch_attempted so the worker can queue it; find_pending_bounded reads every .alive name, 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::Full discarded unique evidence; worker skipped via launch_attempted fully addressed — no discard, gen removed from launch_attempted; a_full_queue_defers_a_unique_crash_to_the_worker_instead_of_discarding_it covers the process-lives path
P2 scan_cap in readdir order before sort fully addressed
P2 newest-first starves MAX_RECOVERY_ATTEMPTS partially addressed — retries are in the scan set, but the pass still dies on the newest before bumping them

Remaining findings

P2 — Admission::Full still counts as a recovery attempt, so a crash loop with a full queue deletes unique evidence after 4 launches

bump_attempts runs at claim, before admit_recovered. Full un-claims for the worker and returns without discard, but the sidecar already went 1→2→3. On the 4th launch attempts > 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 (abort right after recover_on_launch_path returns) never lets the worker run. Upload already failing with 64 reports (or ~64 MiB) in queue/ 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 .attempts gens in the scan set does not make MAX_RECOVERY_ATTEMPTS progress

find_pending_bounded can return [newest, …, original], and a_session_with_failed_attempts_is_not_starved_by_newer_ones asserts that. run_launch_recovery still processes that vec newest-first. A native fault / abort in recover_one of the newest (host before_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_pending now 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

  1. Do not count Full as a recovery attempt (restore the previous .attempts value, or bump only after Admit / intentional drop / assemble). Leave a capped-out unique session untouched until the queue can take it.
  2. 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 to MAX_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_send threading docs; CI green on this head.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread crates/bugsee-core/src/runtime.rs
Comment thread crates/bugsee-core/src/runtime.rs
…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)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 4530024

Both remaining P2s from 6d6faa2 are fixed.

  • Admission::Full is not an attempt. undo_attempt restores 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 and discard unique evidence. Unit test an_undone_attempt_is_not_counted covers the counter; the process-lives Full path still lands on the worker.
  • Already-tried sessions run first. find_pending_bounded returns .attempts gens before fresh newest-first ([2, 6, 5]). A native fault/abort while recovering the newest still leaves the original at the head of the next launch’s list, so it can be queued or reach MAX_RECOVERY_ATTEMPTS. The worker uses the same order via find_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 Full discarded unique evidence fully addressed (earlier)
P2 scan-cap before newest-first sort fully addressed (earlier)
P2 Full burned MAX_RECOVERY_ATTEMPTS fully 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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 4530024

Both leftover P2s from 6d6faa2 are fixed. Admission::Full now gives the attempt back (undo_attempt), and find_pending_bounded returns already-tried sessions first (test: [2, 6, 5]), so a fault while recovering the newest still drives the original to MAX_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::enqueue failing — 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::Full discarded unique evidence; worker skipped via launch_attempted fully addressed
P2 scan_cap in readdir order before sort fully addressed
P2 newest-first starves MAX_RECOVERY_ATTEMPTS fully addressed — retries are processed first
P2 Full burned attempts so a crash loop deleted a unique crash on launch 4 fully addressed — undo_attempt

Remaining finding

P2 — enqueue failure after Admit still counts as a recovery attempt, so a crash loop on an almost-full disk deletes unique evidence after 4 launches

admit_recovered returns Full only 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 is Admitted. recover_claimed has bumped .attempts (a few bytes under sessions/ — that write fits). assemble_with_extras builds the zip in memory. queue::enqueue then fails on the bundle. The Full arm is not taken: no undo_attempt, gen stays in launch_attempted.

#10’s loop (abort right after recover_on_launch_path) never lets the worker run and throws away hold_in_memory. Launch 4 hits attempts > 3 and discards 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

  1. Treat queue::enqueue failure like Admission::Full: undo_attempt, and drop the gen from launch_attempted so a living process’s worker can retry. Leave assemble errors 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_send threading docs; CI green on this head.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread crates/bugsee-core/src/runtime.rs

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 4002558

The leftover P2 is fixed. On queue::enqueue failure, recover_one now undo_attempts (same class of refusal as Admission::Full). A crash loop on an almost-full volume can no longer tick .attempts to 4 and discard a unique crash that was never queued. assemble errors still count as poison. The launch pass keeps the gen in launch_attempted so a living process does not hold_in_memory twice — 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 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full burned attempts fully addressed
P2 enqueue failure burned attempts fully 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 launch with the helper-thread budget after the crash handlers are installed.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at b606938

The leftover P2 is fixed. Admission::Full now sets launch_deferred. After launch_done, worker_loop swaps that flag and runs run_recovery again (claims already released; launch_attempted no longer contains the Full gen). A before_send that outruns budget + 5s can 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_pending empty). 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 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full burned attempts fully addressed
P2 enqueue failure burned attempts fully addressed
P2 Full→worker skipped if helper outran the wait fully addressed (launch_deferred + rescan in worker_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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at b606938

launch_deferred plus a follow-up run_recovery in worker_loop fixes the previous P2 on the writable-disk path: launch_done is stored only after the helper drops its claims, so the worker can pick up a unique crash that Admission::Full released after the budget+5s wait. Prior launch-path P1/P2s still hold.

The follow-up pass is aimed at the wrong directory. worker_loop's data_dir is the live capture tree (store.is_some(), None in memory/degraded mode). Startup recovery already uses drain_dir for exactly that case. swap(false) also consumes the flag when the if 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 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full burned attempts fully addressed
P2 enqueue failure burned attempts fully addressed
P2 Full→worker handoff was one-shot if before_send outran the wait fully 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 Full crash is stranded for the life of a memory-mode process

See the inline comment. #9/#24 already run in memory mode against an existing recovery tree (read-only remount, ENOSPC on Session::begin) while still draining queue/ and recovering sessions. That is the situation where the outbound queue is most likely already at 64. Admission::Full does not hold_in_memory; it only sets launch_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

  1. Run the deferred pass against drain_dir (the same path as the worker's first run_recovery), and do not swap(false) when that path is missing. Cover it with a test: full queue, before_send longer than budget + 5s, and open_disk failing / memory_only with 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_attempt on 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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread crates/bugsee-core/src/runtime.rs Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 first run_recovery), not the live capture data_dir. launch_deferred is only swapped 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_send past 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 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full / enqueue burned attempts fully 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 when Some)

Overall risk: Low

Merge recommendation: approve

Stacked on #32; retarget to main after that merges.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 first run_recovery), not the live capture data_dir. launch_deferred.swap(false) sits inside if 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 slow before_send + Admission::Full; the worker path does not cap, so a unique crash is enqueued or hold_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 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full / enqueue burned attempts fully addressed
P2 Full→worker one-shot after wait fully addressed
P2 deferred pass used capture data_dir (None in memory mode) fully addressed (recover_dir = drain_dir; swap only when Some)

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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 first run_recovery), and launch_deferred.swap(false) only runs when that path is Some. In memory/degraded mode (#9/#24: directory exists, open_disk failed) data_dir is still None, but the follow-up pass no longer no-ops, and it no longer eats the flag when there is nothing to recover.

launch_done is still stored only after the helper drops its claims, so a unique Admission::Full crash that outlived budget + 5s is 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_send past the grace + an unusable-but-present session dir (the existing a_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 Full discarded unique evidence fully addressed
P2 scan_cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full burned attempts fully addressed
P2 enqueue failure burned attempts fully addressed
P2 Full→worker skipped if helper outran the wait fully addressed
P2 deferred recovery used the capture dir (None in memory mode) fully addressed (recover_dir = drain_dir; swap gated on Some)

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.

Open in Web View Automation 

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)

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at bb281f1

Merge of #9 (fix/issue-9) plus a 60s observable wait in a_full_queue_defers_…. This PR's launch-path machinery is intact: undo_attempt on Full and enqueue failure, retries first, launch_deferred + recover_dir (drain tree, swap only when Some). distinct_extra_dirs is a help here: recovering the same tree twice in one launch would double-bump MAX_RECOVERY_ATTEMPTS. Recovered crashes now honour the in-memory blacklist as well as the on-disk one (intentional drop, same as any_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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at bb281f1

Two changes on top of 12d4672: a merge of latest #9/#24 (distinct_extra_dirs, retire_queued on every terminal queue::remove, mem-blacklist consult in recover_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_dir is still drain_dir. mem_blacklisted on recovered crashes matches the worker; the disk .blacklist check was already there.

No remaining P0–P3 with a credible failure scenario. Residual: still no test for Full + before_send past 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 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

Overall risk: Low

Merge recommendation: approve

Stacked on #32; retarget to main after that merges.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at bb281f1

New since the last approve (12d4672): merge of #9/#24 (fbd6644) and a full-queue test that waits on list_pending length with a 60 s bound instead of 10 s.

The #9 merge does not reopen the launch-path holes. recover_dir is still the drain tree (worker_drain_dir), so memory/degraded mode still gets the deferred Full pickup. mem_blacklisted on recover_one is 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_queued are #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 most HEARTBEAT_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_send past 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 Full discarded unique evidence fully addressed
P2 scan_cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full burned attempts fully addressed
P2 enqueue failure burned attempts fully addressed
P2 Full→worker skipped if helper outran the wait fully addressed
P2 deferred recovery used the capture dir (None in 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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_recovery tests) are unchanged from bb281f1. The only facade delta is Bugsee::upload building manual_upload_meta() off the global lock (#25); recover_on_launch_path still runs last, after bugsee_native::install and 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.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at e00b09d

This synchronize is a merge of latest #14 (e01a20a, which itself merged #9 and main, including #23 CrashBackend and #27 hostinfo) into fix/issue-10. Launch-path sources (runtime.rs, recovery.rs, queue.rs, launch_recovery tests) are byte-identical to bb281f1. recover_on_launch_path still runs last in launch_with, after bugsee_native::install (now a CrashBackend wrapper) and the panic hook. The only facade delta on this merge is Bugsee::upload building manual_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.toml from #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] in crates/bugsee/Cargo.toml makes the workspace unloadable

  • Location: crates/bugsee/Cargo.toml:20 (libc = "0.2") and :48 (libc = { version = "0.2", optional = true } plus telemetry = ["dep:sysinfo", "dep:libc"] at :35)
  • Problem: Cargo forbids two tables with the same name. e00b09d kept both: the always-on unix libc this crate already needed (options.rs geteuid, fork tests) and the second table #27 added for hostinfo.
  • Impact: cargo metadata fails; 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-14 did not conflict (the tables sit at different lines). cargo check / CI then: error: duplicate key at 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 restore telemetry = ["dep:sysinfo"]. hostinfo.rs is already cfg(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 Full discarded unique evidence fully addressed
P2 scan_cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full / enqueue burned attempts fully addressed
P2 Full o worker skipped if helper outran the wait fully addressed
P2 deferred recovery used the capture dir (None in 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 metadata succeeds. Unique #10 work does not need another pass after the manifest is collapsed.

Most important to fix

  1. 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_attempt on queue-cap and enqueue-write refusal, launch_deferred on the drain tree, abort-after-launch crash-loop test. CrashBackend install() is still the same entry launch_with uses.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at e00b09d

Merge of origin/fix/issue-14 (e01a20a: #14 + #9 + main #21–#29) onto last-reviewed bb281f1. 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 set source.origin: None, and launch_with still installs the native handler then the panic hook before recover_on_launch_path.

The remarge left the workspace unloadable.

P0 — duplicate [target.'cfg(unix)'.dependencies] tables, so cargo never starts

crates/bugsee/Cargo.toml has both:

  • line 20: libc = "0.2" (required; this crate always calls libc::geteuid from options.rs for the private temp dir)
  • line 48: libc = { version = "0.2", optional = true } plus telemetry = ["dep:sysinfo", "dep:libc"] from #27

TOML forbids two dependencies keys 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 metadata and cargo fmt --all --check fail before any compile. CI 38577801406: MSRV build, Ubuntu format, Windows build, Android clippy all fail in seconds. The same duplicate is on base e01a20a; this remarge imported it (it is not in the three-dot unique diff).

Fix: one unix table, required libc = "0.2". Drop dep:libc from telemetry — the comment on the second table already says libc is in the tree. Optional-only libc would break options.rs when telemetry is off.

Prior findings

Finding At e00b09d
P1 Full discarded unique evidence fully addressed
P2 scan-cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full / enqueue burned attempts fully addressed
P2 Full→worker one-shot after wait fully addressed
P2 deferred pass used capture data_dir fully addressed

Overall risk: High

Unique #10 recovery is still Low. HEAD cannot be built or tested.

Merge recommendation: request changes

Most important to fix

  1. Collapse the two unix libc tables 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_attempt on queue-cap and enqueue-write refusal, Duplicate vs Full, name-then-sort scan, launch_deferred on the drain tree, abort-after-launch crash-loop test. Residual: still no Full + slow before_send + drain-only-dir test.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 66e4f18

The P0 is fixed. One [target.'cfg(unix)'.dependencies] table, required libc = "0.2", and telemetry = ["dep:sysinfo"] (no dep:libc). I confirmed cargo metadata --offline --no-deps succeeds on this head. --no-default-features still gets unix libc for options.rs geteuid. 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 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

Overall risk: Low

Merge recommendation: approve

Stacked on #32; retarget to main after that merges.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 66e4f18

Remarge of #14 after 1ecfca8 (de-duplicate unix libc). Unique launch-path files are unchanged from e00b09d / bb281f1. The only delta is crates/bugsee/Cargo.toml: one [target.'cfg(unix)'.dependencies] table with required libc = "0.2", and telemetry = ["dep:sysinfo"] (no dep:libc).

Confirmed locally: cargo metadata --offline --no-deps exits 0. recover_on_launch_path still runs last in launch_with (after bugsee_native::install and the panic hook).

Prior findings

Finding At 66e4f18
P0 duplicate unix libc tables, workspace unloadable 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: 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 test locally (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_attempt on queue-cap and enqueue-write refusal, launch_deferred on the drain tree, abort-after-launch crash-loop test, and the workspace loads again.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Re-review at 66e4f18

Synchronize is a remarge of latest #32 (fbbabb2, which includes 1ecfca8). Unique launch-path files (runtime.rs, recovery.rs, queue.rs, launch_recovery tests, options.rs) are unchanged from bb281f1. recover_on_launch_path still runs last in launch_with, after bugsee_native::install and the panic hook, outside the facade lock. Recovered reports still set source.origin: None.

The previous P0 is fixed: crates/bugsee/Cargo.toml now has a single [target.'cfg(unix)'.dependencies] table with required libc = "0.2", and telemetry is dep:sysinfo only. Confirmed locally: cargo metadata --offline --no-deps exits 0 (it failed at e00b09d with duplicate 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::Full still undo_attempts, drops the gen from launch_attempted, and sets launch_deferred; enqueue-write refusal still undo_attempts and keeps the claim; deferred pickup still uses recover_dir (drain_dir) and only swaps when that path is Some.

No remaining P0–P3 with a credible failure scenario. Residual unchanged: still no test for Full + before_send past budget + 5s on 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 metadata was run and succeeded.

Prior findings

Finding At 66e4f18
P0 duplicate unix libc tables (unloadable workspace) fully addressed (1ecfca8 on #32, remarged)
P1 Full discarded unique evidence fully addressed
P2 scan_cap before newest-first sort fully addressed
P2 newest-first starved retries fully addressed
P2 Full / enqueue burned attempts fully addressed
P2 Full→worker skipped if helper outran the wait fully addressed
P2 deferred recovery used the capture dir (None in 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.

Open in Web View Automation 

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)
@krassx
krassx changed the base branch from fix/issue-14 to main October 14, 2026 14:32
@krassx
krassx merged commit 6494a4f into main Oct 14, 2026
9 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_with still runs recover_on_launch_path last, after the native handler and panic hook, outside every lock.
  • Helper thread + budget poll (no park); panics contained; launch_done set even if spawn fails.
  • find_pending_bounded: full name scan, sort newest-first, retries (.attempts) processed first, probe cap limit * 16.
  • Admission::Full / enqueue-write failure: undo_attempt, evidence kept; Full also clears launch_attempted and sets launch_deferred.
  • Worker wait is one-shot (budget + 5s); worker_loop then runs a second run_recovery on recover_dir (drain_dir) after launch_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 unix libc = "0.2" table (plus unix dev-dependencies). cargo metadata --offline --no-deps exits 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

  1. Overall risk: Low
  2. Merge recommendation: Already merged; this SHA would have been approved.
  3. Most important issues to fix: None remaining.
  4. 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.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Early-crash recovery must finish on the launch path (avoid crash loops outrunning the report)

1 participant