Address review findings: durability, stale container ids, honest toasts
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m38s
Build App (Preview) / build-windows (pull_request) Successful in 6m18s
Build App (Preview) / build-linux (pull_request) Successful in 7m48s
Build App (Preview) / prune-previews (pull_request) Successful in 1s

An Opus review of the previous commit found several real gaps:

- pending_cleanup::save used plain write-temp-then-rename, unlike
  migration_store's fsync'd write it claimed to mirror — a crash in that
  window left a truncated record that list() would skip forever, silently
  reproducing the exact bug this module exists to fix. Now matches
  migration_store's File::create/write_all/sync_all/rename/sync_dir shape,
  and the tests exercise the real save/list/clear functions against a temp
  dir instead of re-implementing their bodies inline.
- remove_project and rebuild_project_container only ever looked at
  project.container_id, unlike every other container-destroying path in the
  codebase, which falls back to find_existing_container for exactly this
  race (a crash between creating a container and persisting its id). A miss
  here left a container that then blocked every subsequent volume removal
  with a 409, forever. Both now resolve the same way the rest of the
  codebase does, and record the container by its deterministic name rather
  than its id so a retry still has something that resolves.
- remove_project's toast promised an automatic retry unconditionally, even
  when writing the pending-cleanup record itself failed (the one case
  where nothing will actually retry). ProjectRemovalReport now carries
  retry_scheduled, and the UI is honest about which case it's in.
- remove_volumes_by_name now retries once after a short delay on a 409,
  since Docker releasing a volume's mount reference right after its
  container is removed is not always instantaneous, and this is exactly
  the sequence remove_project runs.
- rebuild_project_container (Reset) returns ProjectResetOutcome so the UI
  can warn when Reset could not fully clear a project's volumes, instead
  of only logging it — the new container silently reuses old data
  otherwise, which is what Reset promises not to do.
- retry_pending_cleanup_logged escalates a record's log level after it has
  failed for a week, since recorded_at was otherwise write-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
This commit is contained in:
2026-08-27 08:36:27 -07:00
co-authored by Claude Sonnet 5
parent 4827170715
commit d8bb5ab262
9 changed files with 351 additions and 100 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type { Project, ProjectPath, ProjectRemovalReport, ContainerInfo, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome } from "./types";
import type { Project, ProjectPath, ProjectRemovalReport, ProjectResetOutcome, ContainerInfo, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome } from "./types";
// Docker
export const checkDocker = () => invoke<boolean>("check_docker");
@@ -21,7 +21,7 @@ export const startProjectContainer = (projectId: string) =>
export const stopProjectContainer = (projectId: string) =>
invoke<void>("stop_project_container", { projectId });
export const rebuildProjectContainer = (projectId: string) =>
invoke<Project>("rebuild_project_container", { projectId });
invoke<ProjectResetOutcome>("rebuild_project_container", { projectId });
export const reconcileProjectStatuses = () =>
invoke<Project[]>("reconcile_project_statuses");
+20 -2
View File
@@ -78,12 +78,30 @@ export type ProjectStatus =
| "error";
/** What `removeProject` could not delete. The project is removed from the
* sidebar either way; anything named here is recorded on the host and
* retried automatically the next time the app starts. */
* sidebar either way. When `retry_scheduled` is true, anything named here
* was recorded on the host and will be retried automatically the next time
* the app starts; when false, the record itself could not be saved and
* nothing will retry it. `retry_scheduled` is meaningless when nothing was
* left behind. */
export interface ProjectRemovalReport {
container: string | null;
image: string | null;
volumes: string[];
retry_scheduled: boolean;
}
/** True when a `ProjectRemovalReport` left nothing behind. Mirrors the
* Rust-side `ProjectRemovalReport::is_clean`. */
export function projectRemovalIsClean(report: ProjectRemovalReport): boolean {
return !report.container && !report.image && report.volumes.length === 0;
}
/** What Reset (`rebuildProjectContainer`) produced: the project as it stands
* after restarting, and any volume Reset could not clear — which is reused
* as-is by the new container instead of starting clean. */
export interface ProjectResetOutcome {
project: Project;
leftover_volumes: string[];
}
export type Backend =