Repository navigation
feat: defined shutdown/flush contract and opt-in SIGTERM/SIGINT flush (#8) - #31
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)
Adds crates/bugsee/src/hostinfo.rs with per-platform lookups (libc on unix, hand-declared Win32 calls on Windows; no new dependencies) and threads the results through HostFacts into platform.utc_offset and hardware.model. Closes #16 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… locale) Closes #16 🤖 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)
- Document per exit path what is persisted, delivered and recovered (Bugsee::stop docs, DESIGN.md section 8). - Bugsee::shutdown(timeout) = flush then stop. A stop during a flush now aborts the flush so the clean-stop acknowledgement bound holds. - Bugsee::install_termination_flush: opt-in, chaining SIGTERM/SIGINT hook (self-pipe; flush on a normal thread; re-raises with the default disposition so the exit status is unchanged) and a Windows console control handler. - LaunchOptions::flush_at_exit: atexit-based clean stop for deliberate process::exit. - Subprocess tests for every exit path. Closes #8 🤖 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)
- Detect a handler installed after ours (tokio, signal-hook) by asking the kernel what is in force; flush only, never terminate the host. - A stop no longer waits on the uploader to end the session cleanly (an HTTP call cannot be interrupted); delivery is documented at-least-once. - Fork-child re-install ordering, SIG_IGN left alone, watcher-alive and pending-signal tracking, host sa_mask/SA_ONSTACK/SA_NODEFER/SA_RESETHAND carried over, fork-aware install lock, pipe2, clean rollback on failure. - flush_at_exit is unix only and skipped in fork children. - Flush aborts between deliveries on stop (memory and every queue dir). - Tests: late/SA_SIGINFO/ignored handlers, SIGINT, double signal, fork child, hanging transport; no fixed sleeps; temp dirs cleaned. 🤖 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)
# Conflicts: # crates/bugsee-core/src/runtime.rs
- 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)
The stop-time memory drain relies on the scheduled pass having had its one attempt; only an explicit flush aborts on stop. 🤖 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)
A producer only queues an entry; the capture worker appends it later, so the last events before a fault (still in the channel) were lost. Measured: a 1500-event burst followed by SIGSEGV recovered ~50-60 events. After the marker is complete, the fatal-crash handler now does one atomic ticket, one non-blocking write(2) (Windows: SetEvent) to a pre-created wake handle and polls an atomic for at most 50 ms. A helper thread made at launch posts a Flush through the normal FIFO channel and publishes the worker's ack. No allocation or lock on the crashing thread; a stuck worker costs only the budget. A main-thread panic nothing will catch runs the same bounded flush. Tests: core handshake/stuck-worker/deregistration, torn part-tail recovery, and subprocess crash tests (burst then SIGSEGV recovers every event; wedged worker still dies promptly and reports). fork_safety's FileTransport now writes atomically since a dying worker can linger mid-upload. Closes #13 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review: shutdown/flush contract and opt-in termination hook
Reviewed the stacked diff (
fix/issue-9…c462eef) against issue #8,Recorder::drop/drain_until_empty, the fork protocol, and the subprocess matrix incrates/bugsee/tests/shutdown.rs. This is not a style pass.The contract itself is sound:
flushdrains,stop/Droppersist and drop the marker without uploading,shutdownis flush-then-stop, and a hung HTTP call no longer turns a deliberate stop into a false abnormal-exit. The unix self-pipe + watcher + late-handlersigactionprobe is the right shape, and the 14 child-process tests cover the paths they claim.Three issues should be fixed before merge. I did not run the test suite here (toolchain in this environment is 1.83; MSRV is 1.88).
Findings
- P1 — Windows Clippy is red because
flush_at_exitstate is notcfg(unix)(crates/bugsee/src/api.rs)- P2 —
SA_RESETHANDemulation kills the process on the first signal (crates/bugsee/src/termination.rs)- P2 — Windows Ctrl+C/Break with the hook still files an abnormal-exit (
crates/bugsee/src/termination.rs)CI (at review time)
windows-latest: build OK, tests OK, Clippy failed (-D warnings)android aarch64: Clippy failed onbugsee-core/bugsee-native/bugsee-ffi(job logs were not available; this crate’s new Windows/atexitcode is not in that job)- ubuntu / macos were still running
Overall risk: Medium
Merge recommendation: Request changes
Most important to fix
cfg(unix)theEXIT_FLUSH_*statics and theAtomicU32import so Windows Clippy is green.- Snapshot “should this wakeup terminate?” at handler time (pipe byte), instead of re-reading a slot that
SA_RESETHANDhas already rewritten. Add a LinuxSA_RESETHANDsubprocess test.- On Windows Ctrl+C/Break,
shutdownwhen this handler is the one that will let the process die (or document that hosts must stillstopthemselves, and drop the implication that the hook makes Ctrl+C a clean exit).Residual (not blocking)
- C ABI still has
bugsee_flush/bugsee_stoponly — nobugsee_shutdown/ termination hook. C hosts can compose the two calls; they cannot get the signal helper.DESIGN.md§6 still says the launch guard’sDropflushes; §8 now says it never uploads. Pre-existing inaccuracy, now contradictory.SA_RESETHAND/pipe2/ the Windows handler were not exercised (noted in the PR). The first of those is a real bug, not just a coverage gap.What looks good
Stop no longer gates the marker on the uploader, so a slow PUT cannot invent a crash.
stoppingabortsdrain_until_emptybetween deliveries. Late host handlers (tokio /signal-hook) are detected by asking the kernel, not by trusting the slot. Fork children without a watcherdie()immediately instead of hanging on a dead pipe. HostSA_SIGINFO/sa_mask/SA_ONSTACK/SA_NODEFERare carried through. The subprocess tests actually kill children and relaunch over the same data dir.Sent by Cursor Automation: Bugsee code review
…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)
- Android: errno via libc::__errno (and the BSD variants), not __errno_location. - Gate the flush_at_exit statics and import on unix (Windows clippy). - SA_RESETHAND: the handler decides "terminate" before emulating the reset and ships it in the pipe byte; the watcher no longer re-reads the slot. New test with a one-shot host handler. - Windows Ctrl+C/Break now run shutdown like close/logoff/shutdown; document that a host which swallows the event registers its handler after ours. - DESIGN section 6 no longer says guard Drop flushes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Keeps the file-lock claim.rs (the pid-claim changes from #9 no longer apply; its lenient variant is retained). Writes the F_UNLCK comparison portably and gates the unix-only Lock::Unsupported variant so non-unix clippy is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review: shutdown/flush contract (re-review at e739839)
Re-reviewed the stacked diff (
fix/issue-9…e739839) against issue #8,Recorder::drop/drain_until_empty, the fork protocol, andcrates/bugsee/tests/shutdown.rs. This is not a style pass.Prior findings
All three from the last review are fully addressed:
- P1
EXIT_FLUSH_*/AtomicU32are#[cfg(unix)]. Windows Clippy is green.- P2
SA_RESETHAND: the handler snapshotsterminatebefore rewriting the slot and ships it in the pipe byte (TERMINATE = 0x80). The watcher no longer re-reads the slot.sigterm_oneshot_handlercovers the first-signal-must-not-kill case.- P2 Windows Ctrl+C/Break now call
Bugsee::shutdown; the docs state that a swallowing host must register after the SDK.CI at
e739839(38554525334) is SUCCESS on ubuntu, macos, windows (including Clippy-D warnings), Android/iOS lint, and MSRV.Findings
- P2 —
drain_until_emptystarts a blocking disk upload afterstoppingalready aborted the memory drain (crates/bugsee-core/src/runtime.rs)No P0/P1. I did not run the suite here; the judgement is from the current code plus the green CI run above.
Overall risk: Medium
Merge recommendation: Request changes
Most important to fix
Re-check
stoppingafterdrain_memoryreturns, and again before eachdeliver_queued. When it is set, return immediately so the uploader can takeUploadMsg::Stopand run the 1.5 s memory drain that the contract depends on.Residual (not blocking)
- C ABI still exposes
bugsee_flush/bugsee_stoponly. C hosts can compose the two calls; they cannot getinstall_termination_flush.- Windows console handler and Linux
pipe2are still not exercised by tests (shutdown.rsis#![cfg(unix)]). The Windows path is now the right shape on paper.- macOS still does not report
SA_RESETHAND(already documented on the native crash path). One-shot / BSDsignal()hosts stay on the flush-only side; safer than the old first-signal kill, but a second SIGTERM will not become default-terminate there.- Service-control stop is not a console event; documented, host must call
shutdown.What looks good
The contract is still sound:
flushdrains,stop/Droppersist and drop the marker without uploading,shutdownis flush-then-stop, and a hung HTTP call no longer invents a crash. The unix self-pipe + watcher + late-handlersigactionprobe is the right shape. Fork children without a watcherdie()immediately.flush_at_exitis unix-only and skipped in fork children. The 15 subprocess cases actually kill children and relaunch over the same data dir, including the new oneshot test.Sent by Cursor Automation: Bugsee code review
…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)
- One process-wide wake pipe/event and ONE helper thread, created once and re-created in a fork child; the flush target is swapped under a fork-aware lock (last registered wins). Handles are never closed, so no descriptor-reuse race and no SIGPIPE for C hosts; shared ticket counters are now coherent. - Dying flag raised by the crash handler before waking the helper: claims and recoveries start nothing in the window, so a report is never half-delivered (removes the 5 s UNOWNED_GRACE stall in fork_safety; test-helper workaround reverted). - Skip the wait when the crashing thread is the worker or helper. - Handler waits with poll (Unix) / ack event + QPC (Windows) instead of nanosleep / Sleep(1); pipe2(O_CLOEXEC) on Linux/Android; parent's fds closed in a fork child; unpublish-then-republish ordering. - Panic hook shares DEFAULT_BUDGET; docs cover host-caught panics, ordering, Windows filter order and last-registered-wins. - crash_inflight runs on Windows too (cfg any(unix, windows)); a hidden budget setter lets the burst test use a long budget deterministically, a small burst covers the real 50 ms budget, and the wedged test now requires the handler to wait the budget and no longer (all three fail without the feature). 🤖 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)
Records how each session ends (ok/errored/crashed/abnormal) behind an off-by-default option. Recovered crashes settle their session exactly once. Nothing is sent: the backend has no session-health ingest endpoint. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… 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)
🤖 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)
# Conflicts: # crates/bugsee/Cargo.toml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review: shutdown/flush contract (re-review at 50da593)
Re-reviewed the unique diff versus the current base (
fix/issue-12@5a844b8…50da593) and the merge of a newerorigin/fix/issue-12(native CrashBackend, origin walks, FFI audit,lock_backtrace). Unique shutdown/flush code is unchanged since 3703010. This is not a style pass.What changed this round
50da593remargesorigin/fix/issue-12@5a844b8. Unique delta vs that base is still the eight #8 files (termination.rs,shutdown.rs,api.rslifecycle,options.rsflush_at_exit,drain_until_empty/stopping, DESIGN §8, the storage-failures stop-during-flush test).termination.rsandshutdown.rshave an empty diff against 3703010. The only unique-file edit in the merge itself isBugsee::upload()taking the origin walk off the global lock — that is #1/#12 work, not #8.Prior findings
All four from earlier rounds stay fully addressed:
- P1
EXIT_FLUSH_*/AtomicU32are#[cfg(unix)].- P2
SA_RESETHAND: terminate is decided in the handler and shipped in the pipe byte (TERMINATE = 0x80).sigterm_oneshot_handlercovers the first-signal-must-not-kill case.- P2 Windows Ctrl+C/Break call
Bugsee::shutdown; docs say a swallowing host registers after the SDK.- P2
drain_until_empty(546024b): re-checksstoppingafterdrain_memoryand after each claim, so a stop that landed in the memory pass cannot start a blocking disk PUT.Merge intersection (re-checked against the new base)
Still coherent with #12/#13/#10/#14/#1/#23:
- Marker and session lock still require the capture worker’s ack; health
finish_cleanstill runs on any non-panicking stop. Uploader detach no longer invents a crash.drain_memoryhonors bothstopping(flush abort) andis_dying()(crash-time flush). New disk claims are refused while dying (try_claim_lenient).revive_after_forkstill forgets the parent session and crash-flush helper, so a childshutdowncannot clear the parent’s marker. A fork child that has not re-armed stilldie()s immediately (OWNER_PID/WATCHER_ALIVE).register_exit_flushstill runs beforerecover_on_launch_path; atexit is a no-op until there is something to flush, and a child whose pid does not matchEXIT_FLUSH_PIDdoes not run it.- Native POSIX capture still owns
SIGABRT/SIGSEGV/friends, notSIGTERM/SIGINT. The termination hook and the crash handler do not share a slot.lock_backtrace(origin walks) is not taken on the flush/stop path; a concurrent fork waits the walk out instead of handing the child a stuckbacktracemutex.Tried to break: second signal vs
SA_RESETHAND, late host handler, ignored SIGTERM, fork child without watcher,atexitin a child, stop during an in-flight memory pass then disk PUT, Drop vs health vs marker. Those paths still match the contract and the tests that exist for them.Findings
No remaining unique P0–P3.
CI (run 38577916981 at 50da593)
All 8 checks SUCCESS: ubuntu (includes the unix
shutdown.rssubprocess matrix), windows Build/Test/Clippy, macos, MSRV 1.88, Android aarch64, iOS device and sim, macOS private-API job.I did not run the suite locally (this environment is rustc 1.83; MSRV is 1.88). The judgement is from the current code plus the green CI jobs above.
Overall risk: Low
Merge recommendation: Approve (after/with the stacked #12 base as before).
Residual (not blocking)
- C ABI still exposes
bugsee_flush/bugsee_stoponly. C hosts can compose the two calls; they cannot getinstall_termination_flushorflush_at_exit.bugsee_stop’s rustdoc still says “flushing pending work” — persist + the 1.5 s memory drain, not a disk-queue upload.shutdown.rsis#![cfg(unix)]. The Windows console handler is the right shape and now compiles and Clippy-cleans on windows-latest; it is still not exercised as a subprocess.- macOS still does not report
SA_RESETHAND. One-shot / BSDsignal()hosts stay flush-only after the first signal.- Service-control stop is not a console event; documented, host must call
shutdown.- An HTTP call already in flight cannot be interrupted. That is the documented at-least-once / 2 s detach bound. Scheduled
drain_allstill does not honorstopping(intentional; only an explicit flush aborts).- DESIGN §12 still says
shutdown/install_termination_flush“neither exists on this branch yet”. They do, in §8. Stale #12 wording; not a behavioral bug.What looks good
The contract still matches the code after the stack merge:
flushdrains,stop/Droppersist and drop the marker without uploading,shutdownis flush-then-stop, and a hung HTTP call no longer invents a crash. The unix self-pipe + watcher + late-handler probe is unchanged and still the right shape. Fork children without a watcherdie()immediately. The subprocess matrix still kills children and relaunches over the same data dir. Windows Clippy is green on this tree.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)
# Conflicts: # crates/bugsee/Cargo.toml
🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review: shutdown/flush contract (re-review at f5dcac4)
Re-reviewed the unique diff versus the current base (
fix/issue-12@fe63783…f5dcac4) and the surrounding Drop / drain / crash-flush / fork / native paths. Unique shutdown/flush code is unchanged since 3703010. This is not a style pass.What changed this round
f5dcac4remargesorigin/fix/issue-12@fe63783(libc de-dupe1ecfca8already in that stack). The trees off5dcac4and the last-reviewed50da593are identical (9444ea77); the basesfe63783and5a844b8are likewise identical (5722f5e9). Unique delta vs base is still the eight #8 files (termination.rs,shutdown.rs,api.rslifecycle,options.rsflush_at_exit,drain_until_empty/stopping, DESIGN §8, the storage-failures stop-during-flush test).crates/bugsee/Cargo.tomlhas a single[target.'cfg(unix)'.dependencies]libctable;cargo metadataloads (the duplicate-table P0 that hit sibling stack PRs is not on this tree).Prior findings
All four from earlier rounds stay fully addressed:
- P1
EXIT_FLUSH_*/AtomicU32are#[cfg(unix)].- P2
SA_RESETHAND: terminate is decided in the handler and shipped in the pipe byte (TERMINATE = 0x80).sigterm_oneshot_handlercovers the first-signal-must-not-kill case.- P2 Windows Ctrl+C/Break call
Bugsee::shutdown; docs say a swallowing host registers after the SDK.- P2
drain_until_empty(546024b): re-checksstoppingafterdrain_memoryand after each claim, so a stop that landed in the memory pass cannot start a blocking disk PUT.Merge intersection (re-checked)
Still coherent with #12/#13/#10/#14/#1/#23/#9:
- Marker and session lock still require the capture worker’s ack; health
finish_cleanstill runs on any non-panicking stop. Uploader detach no longer invents a crash. Drop still waits 2 s for the uploader ack; it just does not gate the marker on the result.drain_memoryhonors bothstopping(flush abort) andis_dying()(crash-time flush). New disk claims are refused while dying (try_claim_lenient).revive_after_forkstill forgets the parent session and crash-flush helper, so a childshutdowncannot clear the parent’s marker. A fork child that has not re-armed stilldie()s immediately (OWNER_PID/WATCHER_ALIVE).register_exit_flushstill runs beforerecover_on_launch_path; atexit is a no-op until there is something to flush, and a child whose pid does not matchEXIT_FLUSH_PIDdoes not run it.- Native POSIX capture still owns
SIGABRT/SIGSEGV/friends, notSIGTERM/SIGINT. The termination hook and the crash handler do not share a slot.lock_backtrace(origin walks) is not taken on the flush/stop path.Tried to break, and did not file:
- Ignored
writereturn in the unix handler: on thisO_NONBLOCK1-byte pipe,EINTRis frompipe_wait(full + blocking); a full pipe isEAGAIN; the default-disposition path cannot fillPIPE_BUF.OWNER_PIDis stored afterPIPE_Wwith release/acquire, so a live owner always sees a valid fd. Watcherreadalready retriesEINTR.- Drop vs
is_dying()during a hung upload: Drop still sits inrecv_timeout(2s)for the uploader; a concurrent native crashaborts at the 50 ms budget first, sosession.end()does not run and the marker stays. The unique change (not gating the marker onuploader_acked) only applies after that wait returns.Findings
No remaining unique P0–P3.
CI (run 38579287636 at f5dcac4)
All 8 checks SUCCESS: ubuntu (includes the unix
shutdown.rssubprocess matrix), windows Build/Test/Clippy, macos, MSRV 1.88, Android aarch64, iOS device and sim, macOS private-API job.I did not run the suite locally (this environment is rustc 1.83; MSRV is 1.88). The judgement is from the current code plus the green CI jobs above.
cargo metadata --offlinesucceeds on this tree.Overall risk: Low
Merge recommendation: Approve (after/with the stacked #12 base as before).
Residual (not blocking)
- C ABI still exposes
bugsee_flush/bugsee_stoponly. C hosts can compose the two calls; they cannot getinstall_termination_flushorflush_at_exit.bugsee_stop’s rustdoc still says “flushing pending work” — persist + the 1.5 s memory drain, not a disk-queue upload.shutdown.rsis#![cfg(unix)]. The Windows console handler is the right shape and now compiles and Clippy-cleans on windows-latest; it is still not exercised as a subprocess.- macOS still does not report
SA_RESETHAND. One-shot / BSDsignal()hosts stay flush-only after the first signal.- Service-control stop is not a console event; documented, host must call
shutdown.- An HTTP call already in flight cannot be interrupted. That is the documented at-least-once / 2 s detach bound. Scheduled
drain_allstill does not honorstopping(intentional; only an explicit flush aborts).- DESIGN §12 still says
shutdown/install_termination_flush“neither exists on this branch yet”. They do, in §8. Stale #12 wording; not a behavioral bug.What looks good
The contract still matches the code after the stack merge:
flushdrains,stop/Droppersist and drop the marker without uploading,shutdownis flush-then-stop, and a hung HTTP call no longer invents a crash. The unix self-pipe + watcher + late-handler probe is unchanged and still the right shape. Fork children without a watcherdie()immediately. The subprocess matrix still kills children and relaunches over the same data dir. Windows Clippy is green on this tree.Sent by Cursor Automation: Bugsee code review
Root cause of the windows flake in a_stuck_worker_costs_only_the_budget: the helper's ack wait (250 ms) was shorter than the 5 s budget the test asked for. After un-wedging, the worker had to drain a backlog (file creation on Windows is slow) and the helper gave up before the ack, never retrying, so the handler waited out its whole budget. The helper now waits 10 s (it only bounds a permanently wedged worker). The test also stopped assuming a 150 ms sleep wedges the worker (it waits for the sampler to report entry), uses generous budgets and bounds, and no longer relies on sleeps. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tree unchanged here; the doc clean-up from #36 is applied next. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Generalise the mobile native capture and out-of-process monitor design notes, the FFI audit and a few code comments so they describe behaviour in generic terms (the backend, the ingestion pipeline, existing report variants) instead of naming components of other projects. No behaviour change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tree unchanged: this branch already contains #13's content. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tree unchanged: this branch already contains #12's content. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Deep review: shutdown/flush contract (re-review at fa0a34e)
Re-reviewed the unique diff versus the current GitHub base (main @ 78424d4 … fa0a34e) and the surrounding Drop / drain / crash-flush / fork / health / native paths. Unique shutdown/flush code is unchanged in substance since 3703010. This is not a style pass.
What changed this round
fa0a34e remarges origin/main (squash of #12) after #12 landed; GitHub retargeted this PR to main. Unique delta vs main is still the eight #8 files (termination.rs, shutdown.rs, api.rs lifecycle, options.rs flush_at_exit, drain_until_empty / stopping, DESIGN §8, the storage-failures stop-during-flush test). Against last-reviewed f5dcac4, those files differ only by inherited #36 wording in DESIGN §10 and the already-on-main CRASH_FLUSH_ACK_TIMEOUT 10 s comment in runtime.rs. termination.rs / shutdown.rs / the flush/stop/shutdown API are an empty diff versus f5dcac4.
crates/bugsee/Cargo.toml still has a single [target.'cfg(unix)'.dependencies] libc table; cargo metadata --offline --no-deps loads.
Prior findings
All four from earlier rounds stay fully addressed:
- P1
EXIT_FLUSH_*/AtomicU32are#[cfg(unix)]. - P2
SA_RESETHAND: terminate is decided in the handler and shipped in the pipe byte (TERMINATE = 0x80).sigterm_oneshot_handlercovers the first-signal-must-not-kill case. - P2 Windows Ctrl+C/Break call
Bugsee::shutdown; docs say a swallowing host registers after the SDK. - P2
drain_until_empty(546024b): re-checksstoppingafterdrain_memoryand after each claim, so a stop that landed in the memory pass cannot start a blocking disk PUT.
Merge intersection (re-checked against landed #12 / #13 on main)
Still coherent:
- Marker and session lock still require the capture worker’s ack; health
finish_cleanstill runs on any non-panicking stop. Uploader detach no longer invents a crash. Drop still waits 2 s for the uploader ack; it just does not gate the marker on the result. drain_memoryhonors bothstopping(flush abort) andis_dying()(crash-time flush). New disk claims are refused while dying (try_claim_lenient). Stop-time memory drain still passeshonor_stop = falseso the 1.5 s last attempt is not aborted by the flag it itself depends on.revive_after_forkstill forgets the parent session and crash-flush helper, so a childshutdowncannot clear the parent’s marker. A fork child that has not re-armed stilldie()s immediately (OWNER_PID/WATCHER_ALIVE). Re-arm starts a new watcher and returns because*installedsurvives the fork (ForkMutexdoes not reset the bool), so it does not re-sigactionover itself.register_exit_flushstill runs beforerecover_on_launch_path; atexit is a no-op until there is something to flush, and a child whose pid does not matchEXIT_FLUSH_PIDdoes not run it.- Native POSIX capture still owns
SIGABRT/SIGSEGV/friends, notSIGTERM/SIGINT. The termination hook and the crash handler do not share a slot.
Tried to break, and did not file:
- Ignored
writereturn in the unix handler: on thisO_NONBLOCK1-byte pipe, a full pipe isEAGAINand the default-disposition path cannot fillPIPE_BUF.OWNER_PIDis stored afterPIPE_Wwith release/acquire. - Drop vs
is_dying()during a hung upload: Drop still sits inrecv_timeout(2s)for the uploader; a concurrent native crashaborts at the 50 ms budget first (or immediately oncecrash_flushis dropped), sosession.end()does not run and the marker stays. break 'dirsafter a just-finished disk delivery whenstoppingis set: the next loop iteration returns false at the top; it does not start another PUT.
Findings
No remaining unique P0–P3.
CI (run 38598330393 at fa0a34e)
- ubuntu: SUCCESS (includes the unix
shutdown.rssubprocess matrix) - windows: SUCCESS — Build, Test, and Clippy (
-D warnings) - MSRV 1.88, Android aarch64, iOS sim, macOS private-API: SUCCESS
- macos-latest tests: still in progress at review time. Unique unix tests already passed on ubuntu.
- Prior SHA
f5dcac4run 38579287636 was all 8 SUCCESS.
I did not run the suite locally (this environment is rustc 1.83; MSRV is 1.88). The judgement is from the current code plus the green CI jobs above. cargo metadata --offline succeeds on this tree.
Overall risk: Low
Merge recommendation: Approve
The stacked #12 base is now on main, so this can land on its own.
Residual (not blocking)
- C ABI still exposes
bugsee_flush/bugsee_stoponly. C hosts can compose the two calls; they cannot getinstall_termination_flushorflush_at_exit.bugsee_stop’s rustdoc still says “flushing pending work” — persist + the 1.5 s memory drain, not a disk-queue upload. shutdown.rsis#![cfg(unix)]. The Windows console handler is the right shape and compiles and Clippy-cleans on windows-latest; it is still not exercised as a subprocess.- macOS still does not report
SA_RESETHAND. One-shot / BSDsignal()hosts stay flush-only after the first signal. - Service-control stop is not a console event; documented, host must call
shutdown. - An HTTP call already in flight cannot be interrupted. That is the documented at-least-once / 2 s detach bound. Scheduled
drain_allstill does not honorstopping(intentional; only an explicit flush aborts). - DESIGN §10 still says
shutdown/install_termination_flush“neither exists on this branch yet”. They do, in §8. Stale #12 wording; not a behavioral bug.
What looks good
The contract still matches the code after the remarge onto main: flush drains, stop/Drop persist and drop the marker without uploading, shutdown is flush-then-stop, and a hung HTTP call no longer invents a crash. The unix self-pipe + watcher + late-handler probe is unchanged and still the right shape. Fork children without a watcher die() immediately. The subprocess matrix still kills children and relaunches over the same data dir. Windows Clippy is green on this tree.
Sent by Cursor Automation: Bugsee code review
* feat: degrade to in-memory reporting on read-only/ephemeral file systems (#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) * fix: address review of data-directory fallback (#9) - 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) * fix: second review round for data-directory fallback (#9) - 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) * feat(bugsee): populate utc_offset, hardware model and Windows locale Adds crates/bugsee/src/hostinfo.rs with per-platform lookups (libc on unix, hand-declared Win32 calls on Windows; no new dependencies) and threads the results through HostFacts into platform.utc_offset and hardware.model. Closes #16 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(bugsee): address review of hostinfo (iOS model, tm_gmtoff gating, locale) Closes #16 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): exactly-once recovery and queue draining across processes 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) * feat: define the shutdown/flush contract; opt-in SIGTERM/SIGINT flush - Document per exit path what is persisted, delivered and recovered (Bugsee::stop docs, DESIGN.md section 8). - Bugsee::shutdown(timeout) = flush then stop. A stop during a flush now aborts the flush so the clean-stop acknowledgement bound holds. - Bugsee::install_termination_flush: opt-in, chaining SIGTERM/SIGINT hook (self-pipe; flush on a normal thread; re-raises with the default disposition so the exit status is unchanged) and a Windows console control handler. - LaunchOptions::flush_at_exit: atexit-based clean stop for deliberate process::exit. - Subprocess tests for every exit path. Closes #8 🤖 Generated with [Claude Code](https://claude.com/claude-code) * feat: general PII scrubber and report rate limiting Add a regex-free scrubber (emails, token shapes, keyed secrets, card-like numbers, home-dir usernames; allow/deny/sensitive_keys; off switch) applied at report assembly over every JSON section after before_send, and token bucket rate limiting (per-signature + global, crash floor, bounded memory) applied before the durable queue, surfaced via DropReason::RateLimited and a bugsee.reports_suppressed attribute. Both on by default, opt-out via LaunchOptions::disable_scrubbing / disable_rate_limiting. Closes #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): arbitrate cross-process claims and session liveness with OS 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) * fix: harden the termination hook and shutdown after review - Detect a handler installed after ours (tokio, signal-hook) by asking the kernel what is in force; flush only, never terminate the host. - A stop no longer waits on the uploader to end the session cleanly (an HTTP call cannot be interrupted); delivery is documented at-least-once. - Fork-child re-install ordering, SIG_IGN left alone, watcher-alive and pending-signal tracking, host sa_mask/SA_ONSTACK/SA_NODEFER/SA_RESETHAND carried over, fork-aware install lock, pipe2, clean rollback on failure. - flush_at_exit is unix only and skipped in fork children. - Flush aborts between deliveries on stop (memory and every queue dir). - Tests: late/SA_SIGINFO/ignored handlers, SIGINT, double signal, fork child, hanging transport; no fixed sleeps; temp dirs cleaned. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: keep draining existing data dirs that cannot take new files (#9) - 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) * fix(scrub): second-review hardening of the PII scrubber Numeric secrets under secret keys and integer card numbers are redacted; colon-form prose ("invalid token: expected ident") and "my_app::token:" are left alone; Cookie headers take the whole line; escaped quotes inside quoted values are honoured; the "signatures" exemption moves out of the shared key matcher into crash.json handling; tighter token shapes (AWS, hf_, sk-, prose after Bearer); contextual stem exclusions (author, tokenizer, session_state, method_signature); exact key/sig only redact credential-looking strings; request.json no longer double-scrubs metadata; file names (icon@2x.png) and /Users/Shared are not PII; the pre-check handles \u escapes and separated key stems. Test secrets are assembled from halves for push protection. Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): harden the file-lock claims (second review) - 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) * fix: keep the scheduled memory drain independent of the stop flag The stop-time memory drain relies on the scheduled pass having had its one attempt; only an explicit flush aborts on stop. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * test: gate unix-only helpers so Windows clippy stays clean (#9) 🤖 Generated with [Claude Code](https://claude.com/claude-code) * feat(native): best-effort flush of in-flight events at crash time A producer only queues an entry; the capture worker appends it later, so the last events before a fault (still in the channel) were lost. Measured: a 1500-event burst followed by SIGSEGV recovered ~50-60 events. After the marker is complete, the fatal-crash handler now does one atomic ticket, one non-blocking write(2) (Windows: SetEvent) to a pre-created wake handle and polls an atomic for at most 50 ms. A helper thread made at launch posts a Flush through the normal FIFO channel and publishes the worker's ack. No allocation or lock on the crashing thread; a stuck worker costs only the budget. A main-thread panic nothing will catch runs the same bounded flush. Tests: core handshake/stuck-worker/deregistration, torn part-tail recovery, and subprocess crash tests (burst then SIGSEGV recovers every event; wedged worker still dies promptly and reports). fork_safety's FileTransport now writes atomically since a dying worker can linger mid-upload. Closes #13 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): queue the previous crash on the launch path before launch 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) * fix: drain every trusted unwritable default tree, not just one; stale 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) * fix(scrub,runtime): second re-review fixes Stem exclusions for cancellation_token, www-authenticate/authentication, token_endpoint/url/uri/expiry/ttl; the colon-prose leniency applies only to bare 'token', so password/passwd/secret/api_key values are always redacted; the rate limiter now runs before the on-disk blacklist read so a denied storm is lock + hash only (test counts disk reads). Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: PR #31 review and CI fixes - Android: errno via libc::__errno (and the BSD variants), not __errno_location. - Gate the flush_at_exit statics and import on unix (Windows clippy). - SA_RESETHAND: the handler decides "terminate" before emulating the reset and ships it in the pipe byte; the watcher no longer re-reads the slot. New test with a one-shot host handler. - Windows Ctrl+C/Break now run shutdown like close/logoff/shutdown; document that a host which swallows the event registers its handler after ours. - DESIGN section 6 no longer says guard Drop flushes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): run launch-path recovery on a bounded helper, after the crash 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) * fix(native): harden in-flight crash flush (review feedback, #13) - One process-wide wake pipe/event and ONE helper thread, created once and re-created in a fork child; the flush target is swapped under a fork-aware lock (last registered wins). Handles are never closed, so no descriptor-reuse race and no SIGPIPE for C hosts; shared ticket counters are now coherent. - Dying flag raised by the crash handler before waking the helper: claims and recoveries start nothing in the window, so a report is never half-delivered (removes the 5 s UNOWNED_GRACE stall in fork_safety; test-helper workaround reverted). - Skip the wait when the crashing thread is the worker or helper. - Handler waits with poll (Unix) / ack event + QPC (Windows) instead of nanosleep / Sleep(1); pipe2(O_CLOEXEC) on Linux/Android; parent's fds closed in a fork child; unpublish-then-republish ordering. - Panic hook shares DEFAULT_BUDGET; docs cover host-caught panics, ordering, Windows filter order and last-registered-wins. - crash_inflight runs on Windows too (cfg any(unix, windows)); a hidden budget setter lets the burst test use a long budget deterministically, a small burst covers the real 50 ms budget, and the wedged test now requires the handler to wait the budget and no longer (all three fail without the feature). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): review round 3 on lock claims - 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) * feat(health): local session health records for release health (#12) Records how each session ends (ok/errored/crashed/abnormal) behind an off-by-default option. Recovered crashes settle their session exactly once. Nothing is sent: the backend has no session-health ingest endpoint. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): make the lock-less heartbeat robust to clock skew and slow 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) * fix(core): keep unique crashes the launch pass cannot queue; scan newest-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) * fix(health): address review of release health (#12) Register the backend session eagerly at launch when release_health is on, finish records on any non-panicking stop, ignore stale panic snapshots, memory-mode and empty-dir guards, ack tombstones, generation floor, no file I/O under the facade lock, gc, monotonic durations, unstable/hidden API, docs moved to section 10. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): tried sessions first, and a queue-cap refusal is not an attempt - 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) * fix(native): address review of the in-flight flush (#13) - P1: a dying process skips the worker's recovery scan, orphan GC and legacy drains and reaches worker_loop at once, so the queued Flush is answered. - P2: the flush target stays registered until the worker has acked Stop, so a crash during shutdown still waits for queued entries. - P2: Windows wait uses integer QPC math (no floating point in the filter). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: never recover the same directory twice in one launch (#9) - 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) * fix(core): a lock file that vanishes under a scan is not evidence of 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) * fix: a stop during a flush's memory pass must not start a disk upload Re-check `stopping` after the memory drain and after each claim, so the uploader reaches its stop arm and the stop-time memory drain runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(health): keep panic.info for abort correlation, no eager registration in memory mode Address review on #35: the worker-thread snapshot is expired by a detached thread after the process survived 1 s instead of deleted in the hook, and the eager session registration requires a disk session. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): a failed enqueue write is not a recovery attempt 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(health): do not expire panic.info from a hook timer Address re-review on #35: leave the worker-thread snapshot alone (abort correlation) and rule out a surviving process's leftover at recovery with a 5 s tail. Adds a worker-thread aborting-panic case to the crash matrix. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): a session the launch pass defers is picked up even if the pass outlived the worker's wait 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(health): mark a panicking drop so a slow unwind is still a crash Address re-review on #35: panic_is_recent no longer guesses from the clock when the recorder was dropped while unwinding; empty rotation directories no longer count as capture activity. Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): deferred launch-pass recovery uses the recovery directory, not only the capture tree 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(scrub): do not redact public OIDC/OAuth metadata keys authorization_endpoint/url/uri, oauth* names (oauth_token and oauth_client_secret still hit the token/secret stems), auth_method and token_endpoint_auth_* are not secrets. Tests in bugsee-core and bugsee-reqwest (query params and headers). Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix: remember every undeletable retired bundle, not just delivered ones (#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) * test(core): wait on an observable condition in the full-queue test 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(scrub): narrow the oauth exclusion to public metadata oauth_verifier, oauth_pin and a bare oauth key stay redacted again; oauth_client_id/redirect/scope/issuer/endpoint/... and auth_url/uri/endpoint are public. Tests in bugsee-core and bugsee-reqwest. Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Merge origin/fix/issue-9 into fix/issue-11 Adapt the new storage_failures test to assemble_docs' scrubber argument. Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): declare kernel32 calls once (clashing_extern_declarations on Windows) process.rs and crash_flush.rs declared WaitForSingleObject with different handle types. Both now use one win32 module; no other duplicate extern declarations exist in bugsee-core, bugsee-native or bugsee. Verified with a standalone windows-msvc clippy of the affected modules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(scrub): free-text oauth_verifier/oauth_pin and short pin/otp codes Free-text keyed matching now follows the structured matcher for the OAuth 1.0a verifier/PIN and redacts 4-10 digit pin=/otp= values. Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(scrub): structured matching redacts code_verifier (PKCE) Refs #11 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs: keep the corrected native feature comment from main * fix: de-duplicate the unix libc dependency after merging main (#27's optional form vs the unconditional one) 🤖 Generated with [Claude Code](https://claude.com/claude-code) * fix(core): make the crash-flush handshake robust on slow runners Root cause of the windows flake in a_stuck_worker_costs_only_the_budget: the helper's ack wait (250 ms) was shorter than the 5 s budget the test asked for. After un-wedging, the worker had to drain a backlog (file creation on Windows is slow) and the helper gave up before the ack, never retrying, so the handler waited out its whole budget. The helper now waits 10 s (it only bounds a permanently wedged worker). The test also stopped assuming a 150 ms sleep wedges the worker (it waits for the sampler to report entry), uses generous budgets and bounds, and no longer relies on sleeps. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs: describe backend behaviour generically in release-health notes Refs #12 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs: keep design notes generic (no private-component references) (#36) Generalise the mobile native capture and out-of-process monitor design notes, the FFI audit and a few code comments so they describe behaviour in generic terms (the backend, the ingestion pipeline, existing report variants) instead of naming components of other projects. No behaviour change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs: drop numeric server error codes from notes and comments (#37) Comments and notes only; no behaviour change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)


Closes #8. Stacked on #24 (
fix/issue-9) — base isfix/issue-9; retarget tomainafter #24 merges.Summary
flush(timeout)persists everything captured and tries to drain the queue (true only if empty; idempotent; not signal-safe).stop()/guardDroppersists and removes the liveness marker but never uploads — undelivered reports stay in the durable queue (disk mode). NewBugsee::shutdown(timeout)= flush then stop. Exit-path table in DESIGN.md §8 (at-least-once delivery).Bugsee::install_termination_flush(timeout)(unix): async-signal-safe handler that only writes to a self-pipe and chains to the host's previous handler (SA_SIGINFO/plain, mask/ONSTACK/NODEFER carried, SA_RESETHAND emulated); a watcher thread flushes, then restores SIG_DFL and re-raises only if the disposition really is still ours and was default. SIG_IGN is left alone; handlers the host installs after ours are detected (flush only, never kill the host). Second signal kills at once. Windows:SetConsoleCtrlHandlerreturning FALSE (chain on).LaunchOptions::flush_at_exit(timeout)viaatexit(unix; no-op on Windows; skipped in fork children).stoppingaborts in-flight flush/drain.Review
Opus review (13 findings incl. late-installed host handlers, in-flight upload at stop, fork-child pipe race) addressed; 14 subprocess tests in
crates/bugsee/tests/shutdown.rs(unix-only), ~27 s.Not verified
Windows handler and Linux-specific paths (
pipe2) were never run;SA_RESETHANDemulation has no test.Test plan
-D warnings, workspace tests on macOS (skipping the pre-existing hangingfork_safetytest)🤖 Generated with Claude Code