Repository navigation
fix: harden desktop startup and diagnostics - #343
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
WalkthroughThe desktop now validates release inputs, records structured startup diagnostics, handles verified runtime recovery, renders boot progress, exposes diagnostic controls, and supports secure diagnostic bundle creation through the desktop and CLI. ChangesDesktop diagnostics and runtime lifecycle
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| Filename | Overview |
|---|---|
| desktop/src-tauri/src/runtime/resolver.rs | Centralizes ownership-aware runtime attachment, startup, and recovery decisions. |
| desktop/src-tauri/src/diagnostics/export.rs | Adds constrained, no-clobber diagnostic bundle export with private output permissions. |
| internal/cli/app_diagnose.go | Adds structured live and offline desktop diagnosis with explicit bundle consent. |
| internal/daemon/boot.go | Moves daemon discovery publication behind fallible initialization and readiness. |
| .github/workflows/release.yml | Adds packaged desktop smoke jobs as prerequisites for publication and finalization. |
| scripts/smoke-desktop-release-artifact.sh | Validates clean-home packaged startup, runtime identity, diagnostics, and teardown. |
Sequence Diagram
sequenceDiagram
participant Desktop
participant Resolver
participant Daemon
participant Diagnostics
participant ReleaseCI
Desktop->>Resolver: Resolve installed or running runtime
Resolver->>Daemon: Verify PID, start time, hash, and provenance
alt Compatible daemon is ready
Daemon-->>Desktop: Identity and product origin
else No compatible daemon
Resolver->>Daemon: Provision and start managed runtime
Daemon-->>Desktop: Publish readiness after transports start
else Startup fails
Resolver-->>Desktop: Typed safe startup error
Desktop->>Diagnostics: Persist report and offer retry/export
end
ReleaseCI->>Desktop: Smoke packaged app with clean home
Desktop-->>ReleaseCI: Runtime and diagnostic metadata
ReleaseCI->>ReleaseCI: Permit publication only after smoke passes
Reviews (3): Last reviewed commit: "fix: remove review teardown injection ri..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (15)
internal/daemon/restart_integration_test.go (1)
199-201: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert the persisted replacement start time.
Lines 199-201 inject the replacement process start time, but the test does not verify that the restart operation stores it. A regression that omits or replaces this identity value can still pass. Assert that the persisted replacement start-time field equals the injected value.
As per path instructions, “Ensure tests verify behavior outcomes, not just function calls.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/restart_integration_test.go` around lines 199 - 201, Update the restart integration test around the processStartedAt stub to retain the injected timestamp and assert that the persisted replacement start-time field equals that exact value after the restart operation. Verify the stored outcome rather than only the injected function call, while preserving the existing restart assertions.Source: Path instructions
desktop/src-tauri/src/health_monitor.rs (1)
279-322: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the negative case for boot-timestamp drift.
The test asserts only
is_some().FakeProbereturns a Compozy identity for every call, so the assertion passes whenever the drift branch attaches, including when ownership verification is removed. Add a case where the executable is not app-owned, or where the provenance hash does not match, and assertobserved_identityreturnsNone. Also assert that the returned identity carries the recorded PID, so the test fails when the branch attaches to the wrong process.💚 Suggested additional assertions
- assert!(observed_identity(&home, &processes, &probe).is_some()); + let observed = observed_identity(&home, &processes, &probe).expect("drift runtime attaches"); + assert_eq!(observed.record.pid, record.pid); + + let foreign = FakeProcesses { + starts: HashMap::from([(record.pid, observed_start)]), + executables: HashMap::from([(record.pid, PathBuf::from("/usr/bin/other"))]), + }; + assert!(observed_identity(&home, &foreign, &probe).is_none());As per path instructions: "Verify tests can fail when business logic changes".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/health_monitor.rs` around lines 279 - 322, Add negative coverage to the test around observed_identity: use a non-app-owned executable or mismatched provenance hash and assert it returns None, while retaining the valid case and asserting the returned identity contains record.pid. Ensure the test verifies both ownership/provenance rejection and correct PID attachment for the forward boot-timestamp drift branch.Source: Path instructions
desktop/src-tauri/src/shell.rs (1)
235-292: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExtract the shared
RecordedDaemonStarthandling.The
AwaitingandStartTimeMismatcharms are identical except for the recovery call. Both repeat therecovery_attemptedguard and the four-outcome match. A small helper removes the duplication and keeps the two paths in sync when outcomes change.♻️ Suggested consolidation
+ fn handle_recorded_start( + &self, + recovery_attempted: &mut bool, + start: impl FnOnce() -> RecordedDaemonStart, + ) -> bool { + if *recovery_attempted { + self.publish_error(ShellErrorCode::RuntimeStartFailed); + return false; + } + match start() { + RecordedDaemonStart::Attached { identity, owned } => { + self.finish_attach(*identity, owned); + false + } + RecordedDaemonStart::Recovered => { + *recovery_attempted = true; + true + } + RecordedDaemonStart::Unverified => { + self.publish_error(ShellErrorCode::RuntimeUnhealthy); + false + } + RecordedDaemonStart::Failed => { + self.publish_error(ShellErrorCode::RuntimeStartFailed); + false + } + } + }Each arm then calls the helper and continues the loop only when it returns
true.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/shell.rs` around lines 235 - 292, Extract the shared recovery-attempt guard and RecordedDaemonStart outcome match into a helper near the surrounding shell logic, accepting the appropriate recovery result and returning whether the caller should continue. Update both Resolution::Awaiting and Resolution::StartTimeMismatch arms to call this helper, preserving their distinct recovery functions and existing attach, retry, unhealthy, and failure behavior.desktop/src-tauri/src/diagnostics/export_tests.rs (1)
140-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test for the size limit.
The suite covers consent, redaction, permissions, collisions, and symlinks.
DiagnosticExportError::TooLargeand the removal of the oversized bundle at export.rs lines 72-75 are untested. Write a log file large enough to exceedMAX_DIAGNOSTIC_BUNDLE_BYTES, then assert both the error and that no bundle file remains.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/diagnostics/export_tests.rs` around lines 140 - 195, The diagnostic export tests need coverage for oversized bundles. Add a test alongside the existing export tests that creates a log file exceeding MAX_DIAGNOSTIC_BUNDLE_BYTES, invokes export, asserts DiagnosticExportError::TooLarge, and verifies the generated bundle path does not remain.internal/cli/app_diagnose_bundle_logs.go (2)
129-148: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueConfirm the intended behavior when a line exceeds the remaining budget.
The loop calls
continuewhen a redacted line would crossappDiagnosticBundleLogTailMaxBytes. Later, shorter lines are still appended. The result is a tail with gaps in the middle rather than a contiguous suffix. If a contiguous tail is the contract for the support bundle, usebreakinstead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_diagnose_bundle_logs.go` around lines 129 - 148, The redacted log tail should remain a contiguous suffix when a line exceeds the remaining appDiagnosticBundleLogTailMaxBytes budget. Update redactAppDiagnosticBundleLogTail to stop processing subsequent lines at that boundary instead of skipping the oversized line and appending later lines.
207-226: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueResolve the user home directory once per tail, not once per line.
redactAppDiagnosticBundleLogMessagecallsos.UserHomeDir()for every log line. Pass the resolved home fromredactAppDiagnosticBundleLogTailinstead. This also makes the failure behavior explicit: today aos.UserHomeDir()error silently drops every line in the tail.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_diagnose_bundle_logs.go` around lines 207 - 226, Resolve os.UserHomeDir once in redactAppDiagnosticBundleLogTail and pass the result into redactAppDiagnosticBundleLogMessage for each line. Update the redaction helper signature and remove its per-line home-directory lookup, while preserving explicit handling of resolution errors in the tail flow rather than silently dropping individual lines.internal/cli/app_diagnose_bundle_archive.go (1)
219-221: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReport an empty bundle file with an accurate message.
The size check returns "exceeds its size limit" for both an empty file and an oversized file. An operator who receives this message for a 0-byte bundle gets misleading guidance. Split the two conditions.
♻️ Proposed message split
- if info.Size() < 1 || info.Size() > appDiagnosticBundleMaxBytes { - return errors.New("app diagnose: app control bundle exceeds its size limit") - } + if info.Size() < 1 { + return errors.New("app diagnose: app control bundle is empty") + } + if info.Size() > appDiagnosticBundleMaxBytes { + return errors.New("app diagnose: app control bundle exceeds its size limit") + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_diagnose_bundle_archive.go` around lines 219 - 221, Update the size validation in the app diagnose bundle archive flow to handle info.Size() < 1 separately from info.Size() > appDiagnosticBundleMaxBytes: return an accurate empty-bundle error for zero-byte files, while preserving the existing size-limit error for oversized bundles.internal/cli/app_test.go (1)
345-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the remaining archive validation rules.
This subtest covers the unexpected-entry rejection only.
validateAppDiagnosticBundleArchiveEntryininternal/cli/app_diagnose_bundle_archive.goalso rejects duplicate entries, oversized manifest entries, oversized log entries, and an aggregate size aboveappDiagnosticBundleContentMaxBytes. These are the boundary rules that protect the bundle reader from a hostile archive. Extend this suite with table cases for each rule.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_test.go` around lines 345 - 379, Extend the diagnostic bundle archive validation tests around the existing “Should reject an unexpected live bundle entry” subtest with table-driven cases covering duplicate entries, manifest entries exceeding their limit, log entries exceeding their limit, and aggregate content exceeding appDiagnosticBundleContentMaxBytes. Build valid archive fixtures for each case, invoke readAppDiagnosticBundleManifest, and assert rejection with the appropriate validation error.Source: Path instructions
desktop/src-tauri/src/diagnostics/report.rs (1)
75-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShare one phase vocabulary with the startup marker.
boot_phase_from_stateproduces the phase strings, andis_diagnostic_phaseindiagnostics/startup_marker.rsvalidates them from a second hardcoded list. If one list changes,marker_document_is_validsilently rejects valid markers and previous-crash data disappears. Consider a shared constant slice, or makeis_diagnostic_phasereuse the same source list.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/diagnostics/report.rs` around lines 75 - 87, The phase vocabulary in boot_phase_from_state and is_diagnostic_phase is duplicated; centralize it so both validation and state mapping use the same source of truth. Update the diagnostics phase definition and make is_diagnostic_phase reuse it, while preserving the existing strings returned by boot_phase_from_state and marker_document_is_valid behavior.desktop/src-tauri/src/runtime/update_cadence.rs (1)
6-13: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winThe cadence thread cannot be stopped.
The loop never exits and the thread holds a strong
Arc<DesktopController>. The controller is therefore never dropped, and an update check can start while the application shuts down. Hold aWeakreference so the thread ends after the controller is dropped, or pass an explicit stop flag.♻️ Proposed change using a weak reference
-pub fn spawn(controller: Arc<DesktopController>, interval: Duration) { +pub fn spawn(controller: &Arc<DesktopController>, interval: Duration) { + let controller = Arc::downgrade(controller); std::thread::spawn(move || { loop { std::thread::sleep(interval); - controller.check_updates(); + let Some(controller) = controller.upgrade() else { + return; + }; + controller.check_updates(); } }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/update_cadence.rs` around lines 6 - 13, Update update-cadence::spawn to downgrade the controller Arc to a Weak reference before starting the thread, then upgrade it inside the loop before calling check_updates and exit when upgrading fails. Ensure the spawned thread no longer retains a strong DesktopController reference during shutdown.desktop/src-tauri/src/main.rs (1)
182-197: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winThe panic guard suppresses every panic after the first one.
writing_panicis set totrueand is never reset. After the first panic is written, any later panic on another thread skipslogging::bootstrap_error. The guard appears to be intended as re-entrancy protection, not as a one-shot switch. Reset the flag after the write so later panics are still recorded.♻️ Proposed change to reset the re-entrancy guard
panic::set_hook(Box::new(move |info| { if !writing_panic.swap(true, Ordering::AcqRel) { let message = sanitize_public_text(&info.to_string()); if let Err(error) = logging::bootstrap_error(&logs_dir, &boot_id, "panic", &message) { eprintln!( "{}", sanitize_public_text(&format!("write panic fallback log: {error}")) ); } + writing_panic.store(false, Ordering::Release); } previous_hook(info); }));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/main.rs` around lines 182 - 197, Update install_panic_hook so writing_panic is reset after each attempted logging::bootstrap_error call, while remaining set during the write to prevent re-entrant panic handling. Ensure the reset occurs even when bootstrap_error returns an error, allowing later independent panics to be recorded.desktop/src-tauri/src/runtime/boot_timestamp_drift_tests.rs (1)
161-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for the
Unreachableoutcome.The tests cover
Attached,NotOwned, andIdentityMismatch. No test drivesIdentity::UnreachableintoBoundBootTimestampDrift::Unreachable. That variant is the only outcome a caller can distinguish from a hard mismatch, so a mapping regression would pass unnoticed.♻️ Proposed additional case
let foreign = FakeProbe(Identity::Foreign); + let unreachable = FakeProbe(Identity::Unreachable); @@ + assert!(matches!( + bound_app_owned_runtime_with_boot_timestamp_drift( + &home, &processes, &unreachable, &record, observed + ), + BoundBootTimestampDrift::Unreachable + ));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/boot_timestamp_drift_tests.rs` around lines 161 - 190, Add a test case in should_require_a_compozy_identity_for_a_boot_timestamp_drift_runtime using a FakeProbe configured with Identity::Unreachable, then call bound_app_owned_runtime_with_boot_timestamp_drift and assert it returns BoundBootTimestampDrift::Unreachable. Keep the existing identity mismatch assertions unchanged.desktop/src-tauri/src/runtime/update_lifecycle_tests.rs (1)
305-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo test covers
StalledRuntimeRecovery::Failed.
FakeRecoveryProcesses::force_terminateon line 53 always removes the pid, so the process always exits.stop_processtherefore never returns an error andrecover_verified_runtimenever returnsStalledRuntimeRecovery::Failed. That variant reports an unrecoverable runtime to the user and has no coverage.Add a fake that keeps the process alive after the forced termination.
♻️ Proposed additional case
+#[test] +fn should_report_a_failed_recovery_when_the_runtime_survives_forced_termination() { + let (_directory, home, record) = owned_runtime(); + let processes = FakeRecoveryProcesses { + starts: Mutex::new(HashMap::from([(record.pid, record.started_at)])), + executables: HashMap::from([(record.pid, home.app_binary.clone())]), + graceful_stops: AtomicUsize::new(0), + forced_stops: AtomicUsize::new(0), + stop_on_graceful: false, + stop_on_forced: false, + }; + let delay = FakeDelay; + + assert_eq!( + recover_stalled_runtime( + &home, + &record, + &processes, + &delay, + Duration::from_millis(1), + Duration::from_millis(1), + ), + StalledRuntimeRecovery::Failed + ); + assert!(home.daemon_info.exists()); +}The fake needs a
stop_on_forcedfield that gates the removal on line 55.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/update_lifecycle_tests.rs` around lines 305 - 330, Extend FakeRecoveryProcesses with a stop_on_forced flag and gate the pid removal in force_terminate on that flag, then add a test covering recover_verified_runtime returning StalledRuntimeRecovery::Failed when forced termination leaves the process alive. Keep the existing recovery test configured to stop on forced termination so its Recovered behavior remains unchanged.desktop/src-tauri/src/record.rs (1)
241-255: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThis test asserts only a constructor field assignment.
should_preserve_the_observed_operating_system_process_start_timepassesstarted_atintoAppRecord::newand then reads backrecord.started_at. No business logic can break this assertion. The test cannot fail when startup-time behavior changes.Make it meaningful by asserting the serialized contract, which is what consumers read:
♻️ Proposed change to assert the persisted contract
- let record = AppRecord::new( - 42, - started_at, - report(&ShellState::Resolving), - ShellState::Resolving, - ); - - assert_eq!(record.started_at, started_at); + let record = AppRecord::new( + 42, + started_at, + report(&ShellState::Resolving), + ShellState::Resolving, + ); + + let value = serde_json::to_value(record).expect("record serializes"); + assert_eq!(value["started_at"], "2026-08-11T12:00:00Z");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/record.rs` around lines 241 - 255, Update should_preserve_the_observed_operating_system_process_start_time to serialize the constructed AppRecord and assert the persisted representation contains the observed started_at value, rather than directly comparing record.started_at. Reuse the existing serialization approach and expected field format used by AppRecord consumers or nearby tests.desktop/src-tauri/src/runtime/update_lifecycle.rs (1)
273-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
stopno longer escalates to a forced termination, and it drops the error detail.Two points:
stopcalls onlyself.processes.terminate(pid)and waits.stop_processon line 137 already implements graceful termination, waiting, and forced escalation. A runtime that ignoresSIGTERMnow fails the update instead of being stopped. If the removal of the forced path is intentional, add a short comment that states why the update path must not force-kill.map_err(|_| RuntimeUpdaterError::Lifecycle)on line 288 discards theio::Error. Other failures in this file are logged, for example line 310. Log the cause before mapping.♻️ Proposed change to reuse `stop_process` and log the cause
fn stop(&self, pid: u32) -> Result<(), RuntimeUpdaterError> { let delay = ThreadDelay; - self.processes - .terminate(pid) - .and_then(|()| { - wait_for_exit( - &self.processes, - pid, - &delay, - STOP_DEADLINE, - STOP_POLL_INTERVAL, - ) - .then_some(()) - .ok_or_else(|| io::Error::other("runtime process did not exit after termination")) - }) - .map_err(|_| RuntimeUpdaterError::Lifecycle) + stop_process( + &self.processes, + pid, + &delay, + STOP_DEADLINE, + STOP_POLL_INTERVAL, + ) + .map_err(|error| { + crate::logging::error(format!("stop runtime process {pid}: {error}")); + RuntimeUpdaterError::Lifecycle + }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/update_lifecycle.rs` around lines 273 - 289, Update stop to reuse the existing stop_process flow so runtime termination waits gracefully and escalates to forced termination when necessary, unless the removal is intentional—in that case, document the reason inline. Preserve the wait deadline and interval behavior, and log the underlying termination or wait error before mapping it to RuntimeUpdaterError::Lifecycle instead of discarding it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@desktop/src-tauri/src/controller.rs`:
- Around line 470-472: Restrict the shell_control command in the
command-dispatch logic so it is only callable by the intended authorized
webview, rather than relying on the missing AppManifest::commands registration.
Update export::resolve_output_path, used by export_diagnostics, to accept only
.gz paths contained within home.support_bundles_dir and reject traversal or
outside paths without creating arbitrary parent directories.
In `@desktop/src-tauri/src/diagnostics/actions.rs`:
- Around line 41-59: Update export to validate params.output_path against the
approved home.support_bundles_dir root before calling export::export or creating
any parent directories. Apply the same traversal, symlink-parent, and
containment checks used for the default path to custom paths, rejecting paths
outside CompozyHome’s approved support-bundles directory while preserving valid
custom exports.
In `@desktop/src-tauri/src/diagnostics/startup_marker.rs`:
- Around line 94-133: Validate boot_id at the start of begin_with_owner using
the same constraints enforced by marker_document_is_valid, rejecting empty or
over-long values with an appropriate io::Error before acquiring the lock or
writing the marker. Preserve valid boot behavior; optionally reuse the
already-read previous marker instead of calling read_previous_crash again.
In `@desktop/src-tauri/src/logging.rs`:
- Around line 38-70: Update bootstrap_error to use a platform-specific
open_private_append helper: on Unix create the file with mode 0o600 and
O_NOFOLLOW, while preserving the non-Unix fallback. Change
set_private_file_permissions to operate on the opened fs::File handle rather
than the path, and call it after opening; add a Unix test verifying the
resulting file mode is 0o600, following the startup marker test pattern.
In `@desktop/src-tauri/src/runtime/control_server_startup.rs`:
- Around line 67-71: Update the control-server startup logic around app.manage
and publish_failure so a failed manage call checks whether ControlServer is
already managed: log the duplicate/non-fatal case and return true without
publishing ShellState::ShellError, while retaining publish_failure and a false
result only when no control server remains managed.
In `@desktop/src-tauri/src/runtime/provision.rs`:
- Around line 151-153: Update stage_update to validate the downloaded artifact’s
digest after download_verified and before extract_binary, matching the
integrity-check behavior used by the normal provisioning path. Reuse the
existing artifact validation helper and verified metadata, and propagate
validation failures before extraction proceeds.
In `@desktop/src-tauri/src/runtime/update_lifecycle.rs`:
- Around line 137-168: The process-exit check must distinguish the original
process from a PID-reused process. In
desktop/src-tauri/src/runtime/update_lifecycle.rs:137-168, pass the recorded
started_at through stop_process and wait_for_exit, treating a missing PID or
changed start time as exited; in
desktop/src-tauri/src/runtime/update_lifecycle_tests.rs:15-61, update
FakeRecoveryProcesses so terminate can replace a PID with a different start time
and add coverage asserting force_terminate is not called after reuse.
In `@internal/cli/app_diagnose_bundle_logs.go`:
- Around line 180-205: Decouple the app diagnostic bundle filters from unrelated
CLI constants: update isAppDiagnosticBundleLogLevelAllowed to use a local
constant for the error level, and update
containsAppDiagnosticBundleNonShareableContent to use local constants for the
config and session terms. Preserve the existing matching behavior while ensuring
changes to other CLI output domains cannot alter these allowlists or redaction
terms.
In `@internal/cli/app_diagnose_bundle.go`:
- Around line 59-79: Update createAppDiagnosticBundle to omit output_path when
the operator did not specify --bundle-output, allowing desktop default-directory
hardening to run; preserve the explicit path for custom outputs. Move bundle
path validation ahead of manifest loading by passing the expected path into
decodeAppDiagnosticBundleExport, comparing it before
readAppDiagnosticBundleManifest.
In `@internal/cli/app_test.go`:
- Around line 533-539: The forbidden-content assertions in the diagnostic
archive test are ineffective because “transcript” and “runtime.db” are absent
from its fixtures and do not match production filters. Update the test fixture
setup to write a log line containing an actual filtered term, such as
“compozy.db” or “sqlite”, and assert that term is absent from the archive
alongside the existing forbidden values.
In `@internal/daemon/restart_integration_test.go`:
- Around line 199-201: Update the test case containing d.processStartedAt in
internal/daemon/restart_integration_test.go (lines 199-201) by moving it into a
t.Run("Should ...") subtest and adding t.Parallel(); apply the same structure to
the affected case in internal/daemon/daemon_test.go (lines 1033-1035),
preserving each test’s existing behavior.
In `@scripts/smoke-desktop-release-artifact.sh`:
- Around line 15-18: Update the isolated-runtime lifecycle in the smoke script
around smoke_home, app_pid, and app_process_group to use the required QA
teardown flow instead of ad hoc cleanup. Register long-lived processes under
<QA_OUTPUT_PATH>/qa/pids/, invoke the bootstrap manifest teardown command or
make qa-reap on every exit path, and require a teardown.json result confirming
clean: true before release validation completes.
- Line 141: Update the curl status request in the smoke-test flow to enforce
connection and total timeouts shorter than the 180-second deadline, while
preserving its existing silent, error-reporting, and failure-tolerant behavior.
- Around line 55-61: Update the daemon cleanup flow around stop_process to
compare daemon.json’s started_at with the live process start time and signal the
PID only when they match. Ensure the bootstrap teardown command runs on every
exit path and writes teardown.json with clean: true. Add connection and total
request timeouts to the status curl invocation so it cannot stall past the
180-second deadline.
---
Nitpick comments:
In `@desktop/src-tauri/src/diagnostics/export_tests.rs`:
- Around line 140-195: The diagnostic export tests need coverage for oversized
bundles. Add a test alongside the existing export tests that creates a log file
exceeding MAX_DIAGNOSTIC_BUNDLE_BYTES, invokes export, asserts
DiagnosticExportError::TooLarge, and verifies the generated bundle path does not
remain.
In `@desktop/src-tauri/src/diagnostics/report.rs`:
- Around line 75-87: The phase vocabulary in boot_phase_from_state and
is_diagnostic_phase is duplicated; centralize it so both validation and state
mapping use the same source of truth. Update the diagnostics phase definition
and make is_diagnostic_phase reuse it, while preserving the existing strings
returned by boot_phase_from_state and marker_document_is_valid behavior.
In `@desktop/src-tauri/src/health_monitor.rs`:
- Around line 279-322: Add negative coverage to the test around
observed_identity: use a non-app-owned executable or mismatched provenance hash
and assert it returns None, while retaining the valid case and asserting the
returned identity contains record.pid. Ensure the test verifies both
ownership/provenance rejection and correct PID attachment for the forward
boot-timestamp drift branch.
In `@desktop/src-tauri/src/main.rs`:
- Around line 182-197: Update install_panic_hook so writing_panic is reset after
each attempted logging::bootstrap_error call, while remaining set during the
write to prevent re-entrant panic handling. Ensure the reset occurs even when
bootstrap_error returns an error, allowing later independent panics to be
recorded.
In `@desktop/src-tauri/src/record.rs`:
- Around line 241-255: Update
should_preserve_the_observed_operating_system_process_start_time to serialize
the constructed AppRecord and assert the persisted representation contains the
observed started_at value, rather than directly comparing record.started_at.
Reuse the existing serialization approach and expected field format used by
AppRecord consumers or nearby tests.
In `@desktop/src-tauri/src/runtime/boot_timestamp_drift_tests.rs`:
- Around line 161-190: Add a test case in
should_require_a_compozy_identity_for_a_boot_timestamp_drift_runtime using a
FakeProbe configured with Identity::Unreachable, then call
bound_app_owned_runtime_with_boot_timestamp_drift and assert it returns
BoundBootTimestampDrift::Unreachable. Keep the existing identity mismatch
assertions unchanged.
In `@desktop/src-tauri/src/runtime/update_cadence.rs`:
- Around line 6-13: Update update-cadence::spawn to downgrade the controller Arc
to a Weak reference before starting the thread, then upgrade it inside the loop
before calling check_updates and exit when upgrading fails. Ensure the spawned
thread no longer retains a strong DesktopController reference during shutdown.
In `@desktop/src-tauri/src/runtime/update_lifecycle_tests.rs`:
- Around line 305-330: Extend FakeRecoveryProcesses with a stop_on_forced flag
and gate the pid removal in force_terminate on that flag, then add a test
covering recover_verified_runtime returning StalledRuntimeRecovery::Failed when
forced termination leaves the process alive. Keep the existing recovery test
configured to stop on forced termination so its Recovered behavior remains
unchanged.
In `@desktop/src-tauri/src/runtime/update_lifecycle.rs`:
- Around line 273-289: Update stop to reuse the existing stop_process flow so
runtime termination waits gracefully and escalates to forced termination when
necessary, unless the removal is intentional—in that case, document the reason
inline. Preserve the wait deadline and interval behavior, and log the underlying
termination or wait error before mapping it to RuntimeUpdaterError::Lifecycle
instead of discarding it.
In `@desktop/src-tauri/src/shell.rs`:
- Around line 235-292: Extract the shared recovery-attempt guard and
RecordedDaemonStart outcome match into a helper near the surrounding shell
logic, accepting the appropriate recovery result and returning whether the
caller should continue. Update both Resolution::Awaiting and
Resolution::StartTimeMismatch arms to call this helper, preserving their
distinct recovery functions and existing attach, retry, unhealthy, and failure
behavior.
In `@internal/cli/app_diagnose_bundle_archive.go`:
- Around line 219-221: Update the size validation in the app diagnose bundle
archive flow to handle info.Size() < 1 separately from info.Size() >
appDiagnosticBundleMaxBytes: return an accurate empty-bundle error for zero-byte
files, while preserving the existing size-limit error for oversized bundles.
In `@internal/cli/app_diagnose_bundle_logs.go`:
- Around line 129-148: The redacted log tail should remain a contiguous suffix
when a line exceeds the remaining appDiagnosticBundleLogTailMaxBytes budget.
Update redactAppDiagnosticBundleLogTail to stop processing subsequent lines at
that boundary instead of skipping the oversized line and appending later lines.
- Around line 207-226: Resolve os.UserHomeDir once in
redactAppDiagnosticBundleLogTail and pass the result into
redactAppDiagnosticBundleLogMessage for each line. Update the redaction helper
signature and remove its per-line home-directory lookup, while preserving
explicit handling of resolution errors in the tail flow rather than silently
dropping individual lines.
In `@internal/cli/app_test.go`:
- Around line 345-379: Extend the diagnostic bundle archive validation tests
around the existing “Should reject an unexpected live bundle entry” subtest with
table-driven cases covering duplicate entries, manifest entries exceeding their
limit, log entries exceeding their limit, and aggregate content exceeding
appDiagnosticBundleContentMaxBytes. Build valid archive fixtures for each case,
invoke readAppDiagnosticBundleManifest, and assert rejection with the
appropriate validation error.
In `@internal/daemon/restart_integration_test.go`:
- Around line 199-201: Update the restart integration test around the
processStartedAt stub to retain the injected timestamp and assert that the
persisted replacement start-time field equals that exact value after the restart
operation. Verify the stored outcome rather than only the injected function
call, while preserving the existing restart assertions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6a1a1841-98b7-4613-8300-ea9bb03be23d
⛔ Files ignored due to path filters (24)
.github/workflows/ci.ymlis excluded by!**/*.yml.github/workflows/release.ymlis excluded by!**/*.ymldesktop/schema/app-state.schema.jsonis excluded by!**/*.jsondesktop/schema/fixtures/error.jsonis excluded by!**/*.json,!**/fixtures/**desktop/schema/fixtures/product.jsonis excluded by!**/*.json,!**/fixtures/**desktop/src-tauri/Cargo.lockis excluded by!**/*.lock,!**/*.lockdesktop/src-tauri/Cargo.tomlis excluded by!**/*.tomldocs/qa/automation-backlog/desktop-boot-control-bridge.mdis excluded by!**/*.mddocs/qa/charters/CH-desktop-agent-headless-cli.mdis excluded by!**/*.mddocs/qa/charters/CH-desktop-update-rehearsal-macos.mdis excluded by!**/*.mddocs/qa/journeys/J-desktop-agent-headless.mdis excluded by!**/*.mddocs/qa/reports/2026-08-11-desktop-startup-diagnostics.mdis excluded by!**/*.mddocs/qa/scenarios/APP-agent-cli-app-verbs.mdis excluded by!**/*.mddocs/qa/scenarios/APP-attach-running-daemon.mdis excluded by!**/*.mddocs/qa/scenarios/APP-install-first-run-provision.mdis excluded by!**/*.mddocs/qa/scenarios/APP-start-installed-daemon.mdis excluded by!**/*.mddocs/release/v0.3-single-cut.mdis excluded by!**/*.mdpackages/site/content/docs/cli/app/diagnose.mdxis excluded by!**/*.mdxpackages/site/content/docs/cli/app/index.mdxis excluded by!**/*.mdxpackages/site/content/docs/getting-started/desktop-app.mdxis excluded by!**/*.mdxpackages/site/content/docs/operations/desktop-app.mdxis excluded by!**/*.mdxpackages/site/content/docs/operations/support-bundles.mdxis excluded by!**/*.mdxskills/compozy/references/desktop.mdis excluded by!**/*.mdskills/compozy/references/runtime-operations.mdis excluded by!**/*.md
📒 Files selected for processing (73)
desktop/src-tauri/build.rsdesktop/src-tauri/pages/boot.cssdesktop/src-tauri/pages/boot.htmldesktop/src-tauri/pages/boot.jsdesktop/src-tauri/release_build_contract.rsdesktop/src-tauri/src/app_state.rsdesktop/src-tauri/src/boot_window.rsdesktop/src-tauri/src/controller.rsdesktop/src-tauri/src/diagnostics/actions.rsdesktop/src-tauri/src/diagnostics/export.rsdesktop/src-tauri/src/diagnostics/export_archive.rsdesktop/src-tauri/src/diagnostics/export_tests.rsdesktop/src-tauri/src/diagnostics/mod.rsdesktop/src-tauri/src/diagnostics/report.rsdesktop/src-tauri/src/diagnostics/startup_marker.rsdesktop/src-tauri/src/diagnostics/startup_marker_tests.rsdesktop/src-tauri/src/errors.rsdesktop/src-tauri/src/health_monitor.rsdesktop/src-tauri/src/home.rsdesktop/src-tauri/src/lib.rsdesktop/src-tauri/src/logging.rsdesktop/src-tauri/src/main.rsdesktop/src-tauri/src/record.rsdesktop/src-tauri/src/runtime/artifacts.rsdesktop/src-tauri/src/runtime/boot_timestamp_drift.rsdesktop/src-tauri/src/runtime/boot_timestamp_drift_tests.rsdesktop/src-tauri/src/runtime/control_server_startup.rsdesktop/src-tauri/src/runtime/discovery.rsdesktop/src-tauri/src/runtime/mod.rsdesktop/src-tauri/src/runtime/provision.rsdesktop/src-tauri/src/runtime/readiness.rsdesktop/src-tauri/src/runtime/recorded_start.rsdesktop/src-tauri/src/runtime/resolver.rsdesktop/src-tauri/src/runtime/resolver_tests.rsdesktop/src-tauri/src/runtime/startup_failure.rsdesktop/src-tauri/src/runtime/supervisor.rsdesktop/src-tauri/src/runtime/supervisor_tests.rsdesktop/src-tauri/src/runtime/update_cadence.rsdesktop/src-tauri/src/runtime/update_lifecycle.rsdesktop/src-tauri/src/runtime/update_lifecycle_tests.rsdesktop/src-tauri/src/runtime/update_recovery.rsdesktop/src-tauri/src/shell.rsdesktop/src-tauri/tests/contracts.rsdesktop/src-tauri/tests/runtime_resolution.rsdesktop/src-tauri/tests/update_system.rsinternal/api/core/base_handlers.gointernal/api/core/handlers_test.gointernal/cli/app.gointernal/cli/app_control.gointernal/cli/app_diagnose.gointernal/cli/app_diagnose_bundle.gointernal/cli/app_diagnose_bundle_alias_darwin.gointernal/cli/app_diagnose_bundle_alias_other.gointernal/cli/app_diagnose_bundle_archive.gointernal/cli/app_diagnose_bundle_logs.gointernal/cli/app_diagnose_bundle_sync_unix.gointernal/cli/app_diagnose_bundle_sync_windows.gointernal/cli/app_test.gointernal/cli/app_types.gointernal/cli/roles.gointernal/cli/root.gointernal/cli/tool_artifacts.gointernal/cli/tool_operator_output.gointernal/daemon/boot.gointernal/daemon/boot_extension_publishers.gointernal/daemon/boot_runtime_foundation.gointernal/daemon/boot_runtime_services.gointernal/daemon/daemon.gointernal/daemon/daemon_options.gointernal/daemon/daemon_test.gointernal/daemon/restart_integration_test.goscripts/smoke-desktop-release-artifact.shscripts/verify-desktop-release-build-contract.sh
💤 Files with no reviewable changes (3)
- internal/api/core/base_handlers.go
- internal/daemon/boot_extension_publishers.go
- internal/daemon/boot_runtime_services.go
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/smoke-desktop-release-artifact.sh`:
- Line 32: Replace the string-based TEARDOWN_COMMAND in the teardown flow with a
Bash array containing the script path and each argument as separate elements,
then invoke it as "${teardown_command[@]}" without eval. Preserve the existing
repo_root and manifest_path values while preventing their contents from being
reparsed as shell syntax.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e91601be-27eb-4eb2-a85a-a075a00b9c74
⛔ Files ignored due to path filters (2)
desktop/src-tauri/capabilities/boot.jsonis excluded by!**/*.jsondesktop/src-tauri/permissions/autogenerated/shell_control.tomlis excluded by!**/*.toml
📒 Files selected for processing (24)
desktop/src-tauri/build.rsdesktop/src-tauri/src/diagnostics/export.rsdesktop/src-tauri/src/diagnostics/export_tests.rsdesktop/src-tauri/src/diagnostics/report.rsdesktop/src-tauri/src/diagnostics/startup_marker.rsdesktop/src-tauri/src/diagnostics/startup_marker_tests.rsdesktop/src-tauri/src/health_monitor.rsdesktop/src-tauri/src/logging.rsdesktop/src-tauri/src/main.rsdesktop/src-tauri/src/record.rsdesktop/src-tauri/src/runtime/boot_timestamp_drift_tests.rsdesktop/src-tauri/src/runtime/control_server_startup.rsdesktop/src-tauri/src/runtime/update_cadence.rsdesktop/src-tauri/src/runtime/update_lifecycle.rsdesktop/src-tauri/src/runtime/update_lifecycle_tests.rsdesktop/src-tauri/src/shell.rsdesktop/src-tauri/tests/contracts.rsinternal/cli/app_diagnose_bundle.gointernal/cli/app_diagnose_bundle_archive.gointernal/cli/app_diagnose_bundle_logs.gointernal/cli/app_test.gointernal/daemon/daemon_test.gointernal/daemon/restart_integration_test.goscripts/smoke-desktop-release-artifact.sh
🚧 Files skipped from review as they are similar to previous changes (12)
- desktop/src-tauri/tests/contracts.rs
- desktop/src-tauri/src/runtime/update_cadence.rs
- desktop/src-tauri/build.rs
- desktop/src-tauri/src/health_monitor.rs
- desktop/src-tauri/src/record.rs
- internal/cli/app_diagnose_bundle.go
- desktop/src-tauri/src/runtime/control_server_startup.rs
- desktop/src-tauri/src/main.rs
- desktop/src-tauri/src/runtime/update_lifecycle.rs
- internal/daemon/daemon_test.go
- internal/cli/app_test.go
- desktop/src-tauri/src/diagnostics/startup_marker.rs
Summary
compozy daemon start, while refusing to kill or replace a process whose ownership cannot be provedcompozy app diagnoseRoot cause
The reported
api: stream shutdown bridge not providedwarning was construction-time noise, not the crash cause. The failure involved daemon identity timing, publishing readiness state before startup was actually complete, and release provisioning paths that were not proven from a clean machine. Removing~/.compozyhappened to clear the stale state, but it was not a safe or acceptable recovery mechanism.Changes
Daemon and desktop lifecycle
daemon.jsononly after fallible initialization and transports are readyTERMplus the existing timeout for normal update shutdown; force termination is limited to ownership-proven recoveryVisible startup errors and diagnostics
DiagnosticReportinapp.json, including boot phase, app/runtime versions, safe current error, and correlated previous-crash metadatacompozy app diagnose [-o json]plus--bundle --yes [--bundle-output], with offline fallback only when the desktop socket is absent0600archive containing onlymanifest.jsonand optional redacted current-boot desktop log tails; daemon logs, configuration, credentials, sessions, transcripts, and raw home paths are excludedRelease, docs, and QA
desktop-buildand beforedesktop-publishThe artifact smoke intentionally provisions the current signed production runtime manifest because the immutable package embeds the production channel. Candidate runtime bytes and signatures are verified later by
desktop-publish; publication cannot happen until the package smoke passes.Test plan
make gate-full— full monorepo gate, fingerprint89ed220f2d88122c11399671a97197fcbee8868cclean: truewith no survivorsQA report:
docs/qa/reports/2026-08-11-desktop-startup-diagnostics.mdQA status
BLOCKEDonly for the signed-package verification that cannot be produced from this local working tree. Every locally executable runtime and CLI leg passed. The four affected scenarios remainblocked-verifyuntil the mandatory release artifact matrix runs; they are not recorded as product passes prematurely.Compozy Impact Audit
config.tomlkeys changed; checked runtime config and desktop release configuration. Desktop/runtime references in the official skill were updated for the public CLI behavior.COMPOZY_HOME; no workspace-, session-, or agent-scoped data is included. HTTP, UDS, SSE, cache, and event ownership paths are unchanged, and support bundles explicitly exclude workspace/session content.skills/compozy/references/desktop.mdandskills/compozy/references/runtime-operations.mdfor self-start, typed diagnosis, offline behavior, consent, and support-bundle limits.Release notes
🐛 Bug Fixes
Summary by CodeRabbit
app diagnoseoutput in multiple formats, including consent-based diagnostic bundle export.