Skip to content

v1.3.0: launch hardening and Mac installer - #1

Merged
neokumar1 merged 101 commits into
mainfrom
launch/v1.3.0
Sep 27, 2026
Merged

neokumar1 merged 101 commits into
mainfrom
launch/v1.3.0

Conversation

@neokumar1

Copy link
Copy Markdown
Owner

Draft — launch-readiness audit in progress. First commit carries the prior session's uncommitted hardening, pushed early to verify the macOS 15 test-host crash fix on CI.

🤖 Generated with Claude Code

Neo Kumar and others added 30 commits September 25, 2026 14:25
… prior session)

- Replace isolated deinit (crashes the macOS 15 test host via back-deployment)
  with main-thread teardown; move the playback timer into a MainActor clock.
- Per-analyzer FFT setup ownership; stereo-aware normalization for
  opposite-phase sources; direct reads for interior separation windows.
- Preserve renamed titles against late metadata and publish Now Playing.
- Reserve the drop slot before async provider loading.
- Macro/preset status, clip-LED hold, key transposition, timecodes.
- VoiceOver labels for mixer, transport, and library controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Readable text tokens now meet WCAG AA (>= 4.5:1) on every background and
  channel-strip fill in both modes; previous muted values move to textDisabled.
- accentRed is mode-aware (#FF4245 dark / #C8141C light) with an onAccent
  foreground; add a warning token for comparison and loop markers.
- Follow the system Increase Contrast setting for text and outlines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
safeFilename now trims leading dots, so titles such as "...Baby One More
Time" no longer produce a hidden ZIP and hidden stems. It also replaces
? * " < > | alongside / : \ so shared ZIPs extract in Windows Explorer,
and trims trailing dots and spaces, including after truncation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The peak limiter's look-ahead delayed every mix export by 88 frames and
cut the same number of frames from the end. Render past it and discard
the look-ahead, so a 1x mix stays sample-aligned with the source and the
stems, at the same length.

Time/pitch spreads the last frames past the stretched length and reports
no latency, so at non-1x speed or shifted pitch up to about 80% of a
final hit was lost. Render a fixed 4,096-frame tail whenever time/pitch
is active. Such mixes are now 4,096 frames longer than the stretched
length; the rate 0.5 length assertion is updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cached Demucs stems are Float32 and often peak above 1.0, and baked stem
EQ can push them further. Writing them straight to 24-bit WAV or FLAC
pinned every such sample at full scale.

archive() now renders each stem to a temporary Float32 file while
measuring its post-EQ peak. It then encodes all four with one shared
gain, so the loudest stem peaks at -0.1 dBFS and the relative balance
and stem sum are preserved. Stems already below that level are written
unchanged. archive() returns the applied gain (discardable) so the UI
can mention it. Output formats stay 24-bit WAV and FLAC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render() takes a trailing, defaulted progress callback that receives the
rendered fraction at most once per whole percent, ending at 1. Existing
call sites compile unchanged; the mix export can now show real progress
instead of "RENDERING 0%".

archive() reports continuous progress: stem rendering fills 0-40% and
encoding 40-80%, as before the archive step reports 80%. Stem entries
are now stored rather than deflated, because deflating PCM or FLAC saves
little space and was the slowest part of a stem export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/usr/bin/zip on macOS stores UTF-8 entry names without general purpose
bit 11, so Windows Explorer and other spec-following readers decode a
title such as "Beyoncé" as CP437 mojibake. After zipping, set bit 11 in
the central and local header of each entry whose name is non-ASCII,
valid UTF-8. The stored bytes are already UTF-8 and CRCs cover only file
data, so nothing else changes. Zip64 end records and extra fields are
followed for archives over 4 GB. The step is best effort, so an
unexpected archive layout never fails the export. ASCII-only names are
left as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n cancel

publish() staged its copy as a hidden ".isolate-<UUID>" file beside the
destination, and quitting during a long cross-volume copy left that file
behind. It now stages in the system's item replacement directory on the
destination volume and replaces or moves the file into place from there.
Volumes that cannot provide that directory fall back to the previous
sibling file.

The zip step blocked in waitUntilExit, so it could not be cancelled. It
now polls, terminates zip when the task is cancelled, and reports
cancellation before the exit-status check, so a cancelled export
surfaces as CancellationError and nothing is published. Render and
encode loops already check for cancellation on every block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library used SwiftData's default configuration, which for this
unsandboxed app is the shared ~/Library/Application Support/default.store.
Other unsandboxed SwiftData apps open and migrate that same file, dropping
each other's tables, so launching one could erase Isolate's library (and
Isolate could erase theirs).

The library now lives in Application Support/Isolate/Library.store. On
first launch, rows from a pre-1.3 library are imported from a copy of
default.store (plus -wal/-shm), and only after SQLite confirms the copy
has Isolate's table; the shared file is never opened in place, modified
or deleted.

The container is created explicitly instead of through the scene
modifier, whose failure path silently swapped in an in-memory store. An
unreadable store is moved (never deleted) into a timestamped Library
Backups folder and replaced, and the user is told where it went; only if
that also fails does the session run in memory, with a visible notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enu bar

- Quitting (Cmd-Q or the status menu) while a separation or export is
  running now asks first. Confirming a quit during separation cancels the
  import and waits (up to 15 s) for it to clean up before exiting.
- The status menu's "Open Isolate" and track items targeted
  NSApp.windows.first, which is the status item's own window once the
  main window is closed, so nothing opened. They now use the scene's
  openWindow action, which reopens a closed window, fronts an open one and
  restores a minimized one.
- Automatic window tabbing is off, so the tab bar's "+" can no longer
  create a second main window with duplicated modals and importers.
- The image-only status item has a VoiceOver label and a Playing/Paused
  value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The track-number cleanup stripped leading digits from real titles
  ("7-Eleven" became "Eleven", "1-800-273-8255" lost its "1-"). Only an
  unmistakable "01 - ", "01. " or "01_" prefix before a letter is removed
  now.
- The artwork object is reused while the image is unchanged, and progress
  is republished only when playback departs from what Control Center
  extrapolates (seek, loop wrap, rate or duration change), so the
  once-per-second timer call no longer pushes the full dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sidebar

- Search matched the shared stem file names and the full source path, so
  "bass", "drum", "the", "all" or the account name returned every track.
  It now matches the title, source file name and folder only, without
  the "stem"/"stems"/"all" show-everything special case. The placeholder
  reads SEARCH LIBRARY.
- Media keys, Control Center and the status menu stepped through
  date-added order while the sidebar groups by folder; both now use one
  shared grouping helper, so Next goes to the row below.
- Folders with the same name (two "Greatest Hits") get their parent
  folder in the header.
- The filter runs once per update instead of once per visible row.
- Typing a search closes an open inline track menu, whose invisible
  click catcher otherwise swallowed the next click on the player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- CANCEL IMPORT owned Escape even while About or Settings was drawn over
  the progress screen, so pressing Esc to close About discarded minutes
  of separation. While another card covers it, the cancel button is
  disabled and has no shortcut; Escape closes that card instead.
- About now closes with Escape like the other cards.
- A separation started from a media key closes an open delete card, and
  deletion is refused while a separation runs, so the track being rebuilt
  cannot be removed underneath it.
- Hiding the sidebar closes an open inline track menu, whose invisible
  click catcher otherwise swallowed the next click on the player.
- Renamed titles are capped at 200 characters, and the delete card
  truncates the title so its buttons can't be pushed off screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rwrite silently

- A quarantined app opened from a downloaded DMG runs from a randomized
  AppTranslocation path, so the /Volumes/ check never matched and the
  move prompt never appeared for the real distribution path. The original
  location is resolved first, and only read-only volumes (disk images)
  count, so apps kept on a writable external drive are left alone.
- MOVE & RELAUNCH replaced any /Applications/Isolate.app without asking.
  An existing copy now needs a second, explicit REPLACE & RELAUNCH click
  and is moved to the Trash rather than deleted; an app with a different
  bundle identifier is never touched. The new copy is staged first, so a
  failed copy leaves the installed app alone.
- The installed copy drops the download's quarantine flag, as a copy made
  by Finder would, so it is not translocated again on every launch.
- The prompt no longer promises to eject the installer, and NOT NOW or a
  backdrop click only dismisses it for this launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed cancel

- Red is kept for interrupt and active states only: the loaded track's
  indicator bar, destructive delete, error toast, drop target and
  separation progress, all through theme.accentRed (mode-aware, AA as
  text). IMPORT TRACK, the search focus ring, active-row title, bullets,
  scroller, About badge and bullets, and the neutral primary buttons
  (CLOSE, SAVE, MOVE & RELAUNCH) use text/surface tokens; foregrounds on
  red fills use onAccent.
- Library empty and no-match text used Color.gray literals (down to
  1.7:1 in light mode); they use textSecondary/textMuted.
- Error toasts are announced to VoiceOver and fade instead of sliding
  when Reduce Motion is on; the decorative red glow is gone.
- CANCEL IMPORT was a stock Aqua push button that ignored its red tint.
  It is now a plain DotGothic16 button in the destructive style, and the
  separation screen leads with the percentage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Library store (legacy import from a copied default.store that stays
untouched, another app's store left byte-for-byte, backup and recovery
of an unreadable store, visible in-memory fallback), search and sidebar
order, folder headers, rename bound, Now Playing title cleanup, artwork
reuse and progress republishing, quit confirmation, Escape ownership,
main-window targeting, window tabbing, status item VoiceOver name, and
the move-to-Applications install and translocation checks. All tests use
temporary directories and in-memory or temporary SwiftData stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DemucsEngine.accumulate evaluated MLMultiArray.dataType and dataPointer,
both Objective-C getters, for every one of the 3.5 million samples in a
chunk. Resolve the element type and storage once and walk raw buffers.
The arithmetic and its order are unchanged, so output stays bit-identical;
a test compares both loops on strided Float16 and Float32 tensors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StreamingAudio asked ExtAudioFile for stereo, which keeps only channels
0 and 1 of wider files, so a 5.1 mix lost its centre vocal. Decode every
channel with an explicit client layout (ALAC and AAC otherwise arrive in
codec order) and downmix with AVAudioConverter; files without a declared
layout use the WAV/FLAC default order.

Some decoders end cleanly at a damaged region. For PCM, FLAC and ALAC,
whose declared length is exact, fail with the decoded and expected
durations when more than a second and 1% is missing, instead of caching
part of the song under the file's hash.

Core Audio failures now read as plain language with the four-character
code kept for diagnosis. The cache pipeline version moves to v4 because
multichannel output changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idle model

- A model that exists but fails to load or validate now reports its path
  and the Core ML reason instead of 'CoreML Model Not Found'. Duplicate
  bundle candidates are tried once.
- The model loads in one shared detached task. Cancelling an import stops
  waiting at once; the load finishes in the background and the next
  import reuses it instead of compiling again.
- The model is released after 60 s with no separation running and
  reloaded on demand. In a probe on the development Mac, reloading from
  the compiled cache took 3.2-3.7 s (about 17 s only the first time a new
  build loads it); after inference the process footprint was 1.6-2.7 GB
  and releasing the model brought it to between 40 MB and 1.2 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… iCloud files

- Quitting or crashing mid-separation left .partial-* folders (and rarely
  .backup-*) in the stem cache forever. StemCache.removeAbandonedStaging()
  deletes them; each separation calls it once it holds the single
  separation slot, and DemucsEngine.removeAbandonedStaging() is safe to
  call at launch.
- Before loading the model or decoding, compare the space the cache will
  need (five Float32 stereo files at the declared length, plus headroom)
  with the volume's available capacity and fail with a readable message.
- An evicted iCloud Drive file is downloaded explicitly with a
  'DOWNLOADING FROM ICLOUD...' status. The wait honours Cancel, surfaces
  iCloud download errors, and fails with a clear message after two
  seconds without a network path instead of blocking in a read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Failures in a batch are collected and shown once, naming each file with
  its reason ('2 of 5 files could not be imported. ...'), instead of an
  anonymous toast that the next failure overwrites. A cancelled batch
  says how many remaining files were not imported.
- ImportCoordinator publishes currentFileName, batchIndex and batchCount
  for the progress screen.
- Dropped or chosen folders expand recursively to supported audio files
  in Finder name order, off the main actor; duplicates import once.
  'aifc' replaces 'alac', which is not a file extension.
- New tracks take Finder's display name, so 'AC/DC' is no longer stored
  as 'AC:DC', and Now Playing keeps that title after tag metadata loads.
- A reimport that moves a track to a new cache folder deletes the old one
  when the cache owns it and no other track uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
play(at:) blocks for about one output IO cycle per player, so the fixed
30 ms lead let drums, bass, other and the original start 10-43 ms after
vocals on every play, seek, loop wrap and autoplay. Size the lead from
the device IO buffer, check the calls finished in time, and reschedule
from the same frame with a longer lead if they did not.

Pause now pauses the engine instead of each player, which freezes all
five on one render cycle (no drift across pause/resume), resumes without
play(at:) calls, and releases the output device so the Mac can idle-sleep.
The engine also idles after the track ends and on unload. Imports pause
playback directly because togglePlayback() ignores requests while
splitting. Seeks and unloads flush the time/pitch tail from the old
position; loop wraps keep it. Seeking to the end while looping wraps to
the loop start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shortest A-B loop was 2% of the track (7.2 s on a six-minute song),
so short phrases could not be looped. Use a half-second minimum instead.
Pressing [ at or after B, or ] at or before A, now starts a new region
rather than clamping back into the old one.

Reselecting the loaded track (or Next/Previous in a one-track library)
no longer reloads it and resets the mix, loop, speed and position.
Selecting an entry whose stems and source are both missing shows the
error without unloading a different track that is playing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taps deliver about 100 ms per buffer regardless of the requested 1024
frames, and the spectrum only analysed the oldest 1024 frames. Analyse
every FFT-sized hop, newest first, so recent audio and short transients
register; 1024-frame buffers are processed exactly as before.

Compare Original silences the stem sum after the per-stem taps, so the
channel meters and spectra kept moving for stems nobody could hear.
Clear them when bypass turns on and ignore stem readings while it is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The metadata task inherits the main actor, so full-resolution artwork
decoding and the synchronous AVAudioFile open of the source ran on the
main thread. Decode embedded covers as bounded ImageIO thumbnails (at
most 1024 px, oversized images rejected) and probe the source format on
a worker; results still arrive through the request-ID guard.

Fallback titles use the Finder display name, so "AC/DC" is no longer
shown as "AC:DC" and a hidden extension is not stripped twice. New
imports keep the library title in Now Playing instead of switching to
the tag title until the track is selected again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Shortcuts tab scrolls inside its 345pt frame instead of clipping the
  first and last rows, and lists every real shortcut ([ ], ⌥L, ⌘1-5, ⌘E,
  ⌘, ⌘0, ?, Esc, arrow keys) (design-a11y-2).
- Selected chips and tabs use an inverted neutral style and expose the
  isSelected trait; option groups are labelled (design-a11y-8).
- Replace system red/white/black literals with theme tokens; red stays on
  the ON toggle only (design-a11y-3/-5/-6); reset button fill follows its
  rounded outline (design-a11y-20).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Hover was dead code under allowsHitTesting(false); track it on the
  track hit area within the cap's grab band and drop the unbalanced
  NSCursor push/pop (design-a11y-19).
- Level fill, +6 tick and unlit clip LED use neutral tokens; red marks
  only the lit clip LED and a cap in hand (design-a11y-6).
- Tick marks use theme tokens and follow Increase Contrast (design-a11y-13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lity

Layout (ui-1, ui-9)
- Header reserves a measured share for the STUDIO HUD; the HUD drops track
  metadata, then shortens tab, EQ stem and macro labels, and telemetry
  switches to two-line cards, so nothing truncates from the 860pt
  threshold up (default 1280x800 window shows full tab names).
- Transport picks regular/compact/tight control sizes from the space left
  for the seek bar, which keeps at least 100pt at the 960pt minimum
  window with the library open (was 50pt).
- ⌘1-5 hide the library when the HUD cannot fit beside it, instead of
  switching an invisible mode.

Controls
- Double-click resets pan, EQ knobs and HUD EQ nodes; the tap runs
  alongside the zero-distance drag (ui-5).
- ⌘E global EQ bypass shows "ALL EQ OFF" on every channel strip and "ALL
  BYP" in the HUD; clicking either restores EQ (ui-6).
- Seek bar, LOOP and [ ] L ⌥L do nothing without a loaded track (ui-13).
- Pan readout rounds, and keyboard steps snap to the 5% grid (ui-14).
- HUD EQ node haptic plays once on entering 0 dB (ui-15).
- Clear loop markers with ⌥L or the LOOP context menu (audio-engine-7).
- Escape closes the shortcut cheat sheet; it lists ⌥L and ⌘⌥B and names
  ⌘E correctly (ui-10).

Accessibility and theme
- Labels, values and selected traits for balance M/S, HUD tabs, EQ stem
  pills, macro presets, BYPASS and album art; HUD EQ nodes are adjustable
  and keyboard-focusable; larger hit targets for steppers and chips
  (design-a11y-7/-8/-10/-11/-17/-23).
- Marquee text ends in an ellipsis with a tooltip under Reduce Motion
  (design-a11y-18); rounded fills stay inside their outlines (-20).
- Theme tokens replace system red/yellow/gray literals; red is kept for
  solo/mute, loop, clip, modified values, export in progress and the play
  button; comparison and loop markers use warning (design-a11y-3/-4/-5/-6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Double-clicks and key presses are posted straight to an offscreen window,
so the tests never move the pointer or use the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Neo Kumar and others added 27 commits September 25, 2026 22:44
- README, INSTALL and TROUBLESHOOTING explain that Core ML in macOS 14
  and 15 computes the model incorrectly on some paths, that Isolate
  checks it first, and what to do if it refuses (update to macOS 26).
- AUDIO_ENGINE and MODEL describe the sample-time start and self-test.
- Add the opt-in real-music smoke test as a documented release gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting at the last render time plus a lead could name a frame that had
already passed when the timestamp was stale or rendering ran ahead, and
on hosted macOS 15 some starts never played. Map the host-time start onto
the players' timeline through the last render's sample and host times,
so every player still gets one frame that is a real future moment. A
watchdog confirms the players moved once the start has passed and
otherwise restarts from the same frame on the host-time path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… doc accuracy

- package_release.sh captured codesign output through grep -q under
  pipefail, so SIGPIPE could fail the check at random.
- Run the release workflow on macos-26, where inference is verified;
  hosted macOS 14/15 refuse to separate, which would skip the gate.
- RELEASE.md: merge to main before announcing and bump the cask there.
- Correct cache, batch-summary, equalizer, shortcut-card and staging
  descriptions against the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… started while listing

A batch no longer asks iCloud Drive for every evicted file up front. The
next file is requested from inside the loop while the current one
separates, so Cancel leaves at most one extra download running instead of
the rest of the folder, and prefetched sources no longer compete with the
per-file disk-space preflight. (separation-1)

A library track re-separated while a dropped folder was being listed held
the engine, and every file in the batch then failed at once with "The
import did not finish." The batch now waits for that separation before it
starts. (separation-3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two copies of Isolate at different paths (the DMG copy beside
/Applications, or the move-to-Applications relaunch) share the Stems
cache, and each one's abandoned-staging sweep deleted the other's live
.partial folder, so that separation failed at publish after minutes of
inference.

A separation now holds a non-blocking flock on a .lock file inside its
staging folder until it finishes. The sweep skips a .partial folder whose
lock is held and still removes folders with no lock file or a released
one, since the kernel drops the lock when the owner exits or crashes. The
lock file is removed from the published cache after the move. If the lock
cannot be taken the separation still runs, as before. (separation-2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
folderLabels compared every folder against every other one and ran on
each LibraryView body update, so search keystrokes and SETTINGS hover
stalled the main thread in libraries with hundreds of album folders.
Count each depth's suffixes in a dictionary instead (same labels), and
keep the result in a non-observable cache that rebuilds only when the
set of folders changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- EXPORT: "Cancel export" carries "<n> percent" as its value while
  rendering, and COMPLETED reads "Export complete" with no Cancel
  Export menu item or cancel tooltip. The UI test accepts either
  busy label, since a short render may already show COMPLETED.
- MarqueeText draws with a fixed-size font so the width it measures is
  the width it draws; the artist line uses 11/10 pt, the sizes macOS 27
  already drew for 10.5/9.5. Long album names no longer truncate with
  no tooltip, and the scroll reaches the end of the line.
- The stem waveform's idle center row uses border under Increase
  Contrast so it stays brighter than the unlit dots.
- The compact STUDIO HUD hides the EQ curve's Hz captions, which the
  band nodes and their names covered.
- The shortcut cheat sheet cross-fades under Reduce Motion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… folders

- Only a store SQLite itself reports as damaged (NOTADB/CORRUPT or a failed
  quick_check) is moved to Library Backups. Any other open failure (full disk,
  permissions, a lock, a newer schema) leaves the file in place and runs the
  session in memory with a notice that it was left unchanged.
- If a new store cannot be started after the move, the original files are put
  back so the next launch retries them; if that fails too, the notice names
  the backup folder.
- The backup location is saved and shown at every launch until the user
  closes the notice or the folder is gone (not while still in memory).
- Moving the store aside is all-or-nothing, so a failed move never leaves
  Library.store without its -wal.
- A failed legacy import is reported and retried at up to three launches
  instead of being given up on at once; a failed copy now shows a notice.
- In an in-memory session the "will not be saved" warning is repeated when an
  import batch ends, since each file clears the startup toast.
- Search also matches the folders below the path all tracks share, so artist
  folders shown in group headers match while /Users/<name>/Music does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rden the move prompt

- The quit confirmation now answers .terminateLater from a run-loop timer in
  common modes instead of a Task, so the reply arrives even when terminate
  runs inside a main-queue job (it no longer hangs forever).
- Move & Relaunch quits from a run-loop callout instead of the install Task;
  if the user keeps a separation or export running, the card resets.
- Quitting during an export cancels it and waits up to 5 s for its temporary
  renders and zip process to be cleaned up, as separation already did.
- NOT NOW, Escape and the backdrop cannot hide the card while installing, so
  an install failure is always seen; declining lasts for the whole launch
  even when the window is reopened.
- The replace prompt says when the installed copy is newer, and the second
  click of a double-click can no longer confirm the replacement.
- The completion notification reads "Stems Ready" for a single track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…move prompt

FinalLibraryTests adds regression tests for library-1/2/4/5, shell-1/3/4/6
and export-2: a readable store that fails to open stays in place, a damaged
one is put back when no new store can start (or its backup is named when it
cannot), the recovery notice repeats until closed, a partial move aside is
rolled back, artist folders are searchable but the shared path is not, a
failed legacy copy is retried and then capped, the quit reply arrives from
inside a main-queue job and at its deadline, and the move card cannot be
dismissed while installing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st render

Loop wraps and seeks still waited out the host-time lead (per-player
term, presentation latency and a 5 ms pad), about 0.1 s of silence at a
512-frame buffer. The render-timeline start now uses its own lead: three
output cycles past the newest render on the players' timeline, scaled by
the time/pitch rate, and it retries unless every play(at:) landed with at
least two cycles to spare. The long host-time lead and retry remain only
for the fallback, and the watchdog is unchanged.

The frame is derived from sample time alone. While the engine is paused
the players' sample time stands still but the host time reported with it
keeps the pause as an offset, so mapping a host time through it started
every seek or wrap after a pause late by the pause (0.5 s measured) until
the watchdog switched the session to host-time starts. After the engine
starts, the anchor waits for its first render so it is never the one from
before the stop.

The host-time retry cap also leaves room past the margin on outputs
whose latency alone nears 2 s, so later attempts can succeed.

Measured on this Mac (512 frames at 48 kHz): loop-wrap gaps 32-43 ms,
down from 99-104 ms; a wrap after a 0.8 s pause 32 ms, down from 493 ms
with a watchdog restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reselecting the loaded track returns early to keep its mix, loop, speed
and position, which also swallowed the click once the track had played to
its end. With autoplay on, a stopped track at its end now plays again
from the start, or from the loop start when looping, like the menu and
media-key selection already did. Playing or paused mid-track, reselecting
still changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…endering

Rendering checks for cancellation only at the top of each block, and
publishing did not check at all, so a Cancel clicked at RENDERING 99% or
ARCHIVING 80% (while a cross-volume copy runs) still replaced the
destination and revealed it in Finder. The mix export now checks before
publishing, and publish checks again after its copy to the destination
volume and before the swap; the existing defer removes the staged copy.
Only a cancel during the swap itself can still complete, and then the
file really was replaced, so beginExport keeps reporting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Separation and export hold no power assertion, and pausing through the
engine now releases the output device, so nothing kept an unattended
folder import or long export from idle sleep or App Nap. Each separation
and each export now holds a user-initiated activity that disables idle
system sleep until it finishes, fails or is cancelled. The display may
still sleep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The end of playback arrives after the output device's latency, which is
seconds on AirPlay, and the output idles 0.5 s after that, so a fixed
sleep failed on high-latency outputs. Poll for both, with an 8 s bound,
before the existing assertions. Also state the measured pause regression
precisely in the new loop-wrap test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cancel Export in the File menu stays reachable while an overlay covers
  the EXPORT control.
- Document the render-timeline start and loop-wrap gap, damage-only
  library recovery with in-memory fallback, legacy import retries,
  next-file iCloud prefetch, quit-during-export cancellation, keep-awake
  activity, folder search and replaying a finished song.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@neokumar1
neokumar1 marked this pull request as ready for review September 27, 2026 17:36
@neokumar1 neokumar1 changed the title Launch hardening for v1.3.0 (WIP) v1.3.0: launch hardening and Mac installer Sep 27, 2026
@neokumar1
neokumar1 merged commit b7dd394 into main Sep 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant