[3.5.6] - 2026-04-19
Resource-Lifecycle Hardening
60+ findings from a 5-auditor external and internal review (4x Claude Opus 4.7 + GPT-5.3 Codex) closed across four phases. The sprint delivered a platform, not a patch set — five reusable primitives now absorb the four "leak shapes" the audit surfaced (orphan listeners, spawn-without-handle, MutexGuard across I/O, timer leaks after debounce). Audit grade C to A- across all tracks.
Added
useTauriListenerhook +createTauriListener+guardedUnlisten(src/hooks/useTauriListener.ts): structural answer to the "orphan listener" class. The previoususeEffect → await listen() → return .then(fn => fn())pattern leaked registrations whenever the component unmounted beforelistenresolved. The new hook latches a cancel flag before the await and owns the disposer synchronously.usePointerDraghook (src/hooks/usePointerDrag.ts): replaces 6 sites that attacheddocument-level mousemove/mouseup globals from insideonMouseDownand never cleaned them up on unmount mid-drag.util::AbortOnDrop<T>(src-tauri/src/util/abort_on_drop.rs): wraps aJoinHandle, aborts on drop, exposes.wait()fortokio::select!composition. Replaces everylet _ = tokio::spawn(...)outside explicit daemon sites.util::ProviderGuard(src-tauri/src/util/provider_guard.rs): RAII guard that disconnects the wrappedBox<dyn StorageProvider>on drop, regardless of which?-style error skipped the manual cleanup.util::shutdown_signal(src-tauri/src/util/shutdown.rs): cross-platform first-signal future (SIGINT / SIGTERM / Ctrl+Break). Replacestokio::signal::ctrl_c()which silently ignored SIGTERM — the normal systemd / Docker stop signal.- New
ai_cancel_chatTauri command: plumbsCancellationTokenthrough non-streaming AI requests so "Cancel" actually cancels.
Fixed
- Orphan Tauri listeners (11 sites): App.tsx × 6, CustomTitlebar, CloudPanel × 3, useVaultState × 2, useCloudSync × 3, useTransferEvents × 2, useTraySync × 2, useProviderHealth, PlacesSidebar, SyncScheduler, WatcherStatus, ToolProgressIndicator, SSHTerminal, AIChat, AISettingsPanel, SettingsPanel, SyncPanel. All migrated to
useTauriListener/createTauriListener/guardedUnlisten. provider_connectoverwrite (R2 Critical): the previous provider was abandoned withoutdisconnect()— a dead TCP session leaked until GC. The new implementationawaits the disconnect before the swap.- OAuth callback TCP listener (R2 Critical): the callback task and its listener were leaked on timeout, port open failure, or early error. Now wrapped in
AbortOnDrop+tokio::select!so every exit path releases the port. - MCP progress emitter (R3 Critical): replaced a
tokio::spawn-per-progress-sample leak (measured at 262k spawned tasks under high-volume MCP workloads) with a boundedmpsc::channel(32)+ single consumer. - S3 multipart abort on failure (R2 High): one failed part left siblings running and never issued
AbortMultipartUpload, leaving dangling multipart uploads in the bucket. Now usesJoinSet, aborts siblings on first failure, drains, and callsAbortMultipartUpload. - FTP session pool lease (R2 High):
Dropwithoutrelease()left the next lease with dirty session state; now spawns async reset/disconnect if not released. - MCP pool eviction (R3 High):
last_used_msis nowAtomicU64read outside the lock; disconnect is deferred outside the lock; eviction is refcount-gated so in-use connections survive. - MCP per-profile serialization (R3 High):
HashMap<String, Arc<Mutex<()>>>leaked one lock per unique profile name — nowWeak<Mutex<()>>with prune-on-access. - Retry sleeps honour cancellation (R2 Critical, 5 sites):
provider_commands,provider_transfer_executor,ftp_transfer_executor,lib::background_sync_worker,cross_profile_commands. Previously the user's Cancel meant "finish after the next poll"; now every retry sleep is atokio::select!race against the cancel token. - CLI server shutdown surfaces (R4 Critical × 2, High × 3):
serve http,serve webdav,serve ftp,serve sftp,daemon,mount(Linux FUSE + Windows map-as-drive). All observe SIGINT and SIGTERM;serve sftpdrains per-connection tasks with a 5s grace window; FUSE usesfuser::spawn_mount2and drops theBackgroundSessionon signal (was blockingmount2with no cancel surface). - Plugin hook
kill_on_drop(true)(GPT Critical): plugin children used to outlive panics / timeouts. - AI tools blocking the reactor (R3 High):
local_disk_usage,local_find_duplicates,local_grepnow run insidetokio::task::spawn_blocking. - Monaco / Web Audio / WebGL lifecycle (R1 Medium): Monaco disposables retained and disposed on unmount; Web Audio nodes disconnected and
AudioContext.close()called;AudioPlayerWeakMap entry cleared on unmount. CancellationTokenretry in cross-profile transfers (R2 High):ProviderGuardon every provider; retry sleeps wake on cancel.- Cross-profile plan TTL sweeper (R2 Medium):
approved_planswas only pruned opportunistically onstore_plan/take_plan; a periodic 60s sweeper now prunes expired plans after the 15-minute TTL. TRANSFER_IN_PROGRESSRAII guard (R2 Medium): a panic in the folder-transfer pipeline left the flag true forever, suppressing the filesystem watcher until app restart. A drop-guard restores the flag on every exit path including unwind.- Watcher drop policy (R2 Medium):
try_senddrops are now throttled to one log line per 100 drops via anAtomicU64counter (previously spammed every drop). - MCP per-tool-call timeout (R3 Medium): defense-in-depth wall-clock cap (600 s default, override via
AEROFTP_MCP_TOOL_TIMEOUT_SECS) on the dispatch future, so a wedged provider cannot pin a pool slot indefinitely. - CLI memory DB memoization (R3 Medium):
init_cli_dbused to open a fresh SQLite Connection on every CLI invocation ofagent_memory_*commands, paying WAL + schema-init on each call. Now memoized viaOnceLock<Mutex<Connection>>. - DebugPanel console capture is now reversible (R1 F14 Low):
activateGlobalCapture+activateNetworkCaptureare ref-counted; when the lastDebugPanelunmounts anddebugModeis off, the originalconsole.*and__TAURI_INTERNALS__.invokebindings are restored. - Auto-lock interval stability (P4.1.2): the 30s auto-lock poller used to reset on every
isAppLockedtoggle (React re-schedules on dependency change). Now onlymasterPasswordSettriggers start/stop;isAppLockedis read through a ref. - MyServersPanel scroll timeout cleanup (P4.1.1):
scrollTimeoutwas not cleared on unmount; the 600 ms timeout fired on a detached DOM node. - AudioPlayer retry timeout + AudioVisualizer RAF self-spin + TransferProgressBar / ContextMenu RAF cancel (P4.1.3/4/5): four timer/RAF sites where the handle was not stored in a ref or not cancelled on unmount.
Changed
- Grade deltas (external + internal audit, before → after): Frontend C+ → A-; Backend transfer C+ → A-; AI / MCP / plugins C+ → A; Media / Monaco / CLI C+ → A-; overall C → A-.
ProviderGuardrenamedas_mut/as_reftoprovider_mut/provider_refto avoid clippyshould_implement_trait.
Audit artefacts
docs/dev/audit/2026-04-19-resource-lifecycle/APPENDIX-RETROSPECTIVE.md— primitives, fix index, regression greps, lessons learned.docs/dev/audit/2026-04-19-resource-lifecycle/AUDIT-POST-FIX.md— post-fix review, 6 dimensions, grade A-.
Downloads:
- Windows:
.msiinstaller,.exe, or.zipportable (no installation required) - macOS:
.dmgdisk image - Linux:
.deb,.rpm,.snap, or.AppImage