Skip to content

fix: resolve SIGTRAP crash and blank overlay caused by AsyncParsableCommand#153

Merged
GeneralD merged 7 commits into
mainfrom
fix/152-daemon-main-thread
Apr 4, 2026
Merged

fix: resolve SIGTRAP crash and blank overlay caused by AsyncParsableCommand#153
GeneralD merged 7 commits into
mainfrom
fix/152-daemon-main-thread

Conversation

@GeneralD

@GeneralD GeneralD commented Apr 4, 2026

Copy link
Copy Markdown
Owner

概要

@main AsyncParsableCommandNSApplication.run() と非互換であることが原因で、daemon が SIGTRAP でクラッシュし、オーバーレイが blank になる問題を解決。

変更内容

  • AsyncRunnable プロトコルを AsyncParsableCommand モジュールとして新規作成 — ParsableCommand に async run() をブリッジ(DispatchSemaphore でメインスレッドを解放)
  • RootCommandAsyncParsableCommand → sync ParsableCommand に変更
  • HealthcheckCommand / TrackCommandAsyncRunnable に移行
  • DaemonCommand を v2.5.5 相当の MainActor.assumeIsolated に復元
  • WallpaperPresenter の seekEnd ガード位置を revert(bug: wallpaper not displayed after seekEnd guard optimization in v2.5.6 #154
  • Scripts/check-overlay.swift オーバーレイ描画検証スクリプトを追加
  • CLAUDE.md.claude/CLAUDE.md に移動、AsyncRunnable のドキュメントを追加
  • AsyncParsableCommand モジュールの 7 件のユニットテスト

背景・動機

@main AsyncParsableCommand は Swift の協調スレッドプールを起動し、メインスレッドの実行コンテキストを変更する。NSApplication.run() はメインスレッドのランループを専有する必要があり、両者は根本的に非互換。結果として:

  1. SIGTRAP クラッシュ — async ランタイムが DaemonCommand.run() を非メインスレッドで実行し、MainActor.assumeIsolated が失敗
  2. オーバーレイ blankapp.run() と async ランタイムが競合し、SwiftUI レンダリングが動作しない

AsyncRunnableParsableCommand を拡張して async run() の書き味を保ちつつ、メインスレッドの制御を NSApplication に委ねる。

破壊的変更

  • RootCommandAsyncParsableCommandParsableCommand に変更
  • HealthcheckCommand / TrackCommandAsyncParsableCommandAsyncRunnable に変更(外部から見た動作は同一)

Closes #152
Closes #154

テスト計画

  • swift test 全369テスト合格
  • swift Scripts/check-overlay.swift でオーバーレイ描画を確認
  • lyra track / lyra healthcheck が正常動作
  • brew services start lyra で起動確認(リリース後)

Summary by CodeRabbit

  • Bug Fixes

    • Improved wallpaper seeking behavior to prevent race conditions during playback scrubbing.
  • Chores

    • Version bumped to 2.5.8.

…patibility

MainActor.assumeIsolated crashes with SIGTRAP when the caller is not
on the main thread, which happens under LaunchAgent. DispatchQueue.main.sync
explicitly dispatches to the main thread regardless of the calling context.

Closes #152

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 4, 2026 13:28
@GeneralD GeneralD self-assigned this Apr 4, 2026
@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces AsyncRunnableCommand, a new protocol bridging async run() methods to synchronous ParsableCommand conformance. It updates CLI commands accordingly, fixes a wallpaper rendering regression by restructuring guard logic, adds overlay verification scripts, and bumps the version to 2.5.8.

Changes

Cohort / File(s) Summary
Version Bump
Sources/CLI/Resources/version.txt
Updated CLI version from 2.5.7 to 2.5.8.
Package Manifest
Package.swift
Added new AsyncRunnableCommand library target and AsyncRunnableCommandTests test target with ArgumentParser dependencies; wired AsyncRunnableCommand into the main CLI executable target.
AsyncRunnableCommand Protocol & Implementation
Sources/AsyncRunnableCommand/AsyncRunnableCommand.swift
New protocol conforming to ParsableCommand with required mutating func run() async throws method. Provides default synchronous run() implementation that bridges to async via Task, DispatchSemaphore, and Result to block until completion or capture errors.
AsyncRunnableCommand Tests
Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift
Comprehensive test suite validating async-to-sync bridging, error propagation, concurrent task execution, and thread behavior of AsyncRunnableCommand.
CLI Command Protocol Updates
Sources/CLI/Commands/HealthcheckCommand.swift, Sources/CLI/Commands/TrackCommand.swift
Updated both commands from AsyncParsableCommand to AsyncRunnableCommand conformance; added import AsyncRunnableCommand.
Root Command Entry Point
Sources/CLI/Commands/RootCommand.swift
Changed RootCommand conformance from AsyncParsableCommand to ParsableCommand to serve as synchronous entry point that dispatches to async subcommands.
Wallpaper Rendering Fix
Sources/Presenters/Wallpaper/WallpaperPresenter.swift
Moved time and seeking state guards into Task { @mainactor in ... } block to ensure proper initialization sequence; fixes blank wallpaper regression from v2.5.6.
Overlay Verification Tooling
.claude/scripts/check-overlay.swift
New macOS script querying window metadata via CGWindowListCopyWindowInfo to validate overlay rendering (window bounds, memory usage, onscreen status).
Architecture Documentation
.claude/CLAUDE.md
Updated architecture diagram and documentation to reflect AsyncRunnableCommand/AsyncParsableCommand protocol changes, added "Async Bridge" module description, and integrated overlay verification into build process.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI (ParsableCommand)
    participant Sync as Sync run()
    participant Task as Task (async scope)
    participant Async as Async run()
    participant Sema as DispatchSemaphore
    participant Result as Result<Void, Error>

    CLI->>Sync: invoke run() throws
    Sync->>Sync: create semaphore
    Sync->>Task: Task { `@MainActor` in ... }
    Task->>Async: call await run() async throws
    Async->>Async: execute async logic
    Async-->>Task: return or throw
    Task->>Result: capture success/error
    Task->>Sema: signal()
    Sync->>Sema: wait() blocks
    Sema-->>Sync: unblock
    Sync->>Result: get() rethrow
    Sync-->>CLI: return or throw
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A bridge was built with semaphore's might,
Async commands now run just right!
Wallpaper glows in proper flow,
v2.5.8 steals the show! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: fixing SIGTRAP crash and blank overlay issues caused by AsyncParsableCommand incompatibility.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issues #152 and #154: introduces AsyncRunnableCommand bridging protocol to run async code safely on main thread without async runtime conflicts, changes RootCommand to ParsableCommand, migrates HealthcheckCommand and TrackCommand to AsyncRunnableCommand, and restores the seekEnd guard position in WallpaperPresenter.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives; version bump, documentation updates, test additions, and verification scripts are all supporting changes for the main AsyncRunnableCommand implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/152-daemon-main-thread

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread Sources/CLI/Commands/DaemonCommand.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a SIGTRAP crash when lyra daemon is launched via LaunchAgent by changing how daemon startup code is executed on the main thread.

Changes:

  • Bump CLI version to 2.5.8.
  • Replace MainActor.assumeIsolated with a main-queue dispatch in DaemonCommand.run().

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
Sources/CLI/Commands/DaemonCommand.swift Changes daemon startup to run its AppKit initialization via the main dispatch queue.
Sources/CLI/Resources/version.txt Updates the CLI version string to 2.5.8.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 13 to 15
func run() {
MainActor.assumeIsolated {
DispatchQueue.main.sync {
let app = NSApplication.shared

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DispatchQueue.main.sync will deadlock if run() is invoked on the main thread/queue (e.g., running lyra daemon directly from a terminal, where execution commonly starts on the main thread). Consider guarding with Thread.isMainThread (execute the body directly when already on main) and only using DispatchQueue.main.sync when on a background thread, or switch to an async dispatch + keeping the process alive if you don't need to block the caller thread.

Copilot uses AI. Check for mistakes.
@codecov-commenter

codecov-commenter commented Apr 4, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.33333% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rces/Presenters/Wallpaper/WallpaperPresenter.swift 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ommand

RootCommand was changed to AsyncParsableCommand for lyra track/healthcheck,
but Swift's async runtime takes ownership of the main thread, making
NSApplication.run() unable to drive SwiftUI rendering (blank overlay) and
causing SIGTRAP when MainActor.assumeIsolated is called from a non-main thread.

- Add AsyncRunnable protocol: bridges async run() via DispatchSemaphore
  without starting the async runtime on the main thread
- Revert RootCommand to sync ParsableCommand
- Revert seekEnd guard move that broke wallpaper display (#154)
- Add Scripts/check-overlay.swift for verifying overlay rendering
- Add AsyncParsableCommand module with 7 unit tests

Closes #152
Closes #154

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@GeneralD GeneralD changed the title fix: DaemonCommand SIGTRAP crash under LaunchAgent fix: resolve SIGTRAP crash and blank overlay caused by AsyncParsableCommand Apr 4, 2026
Comment thread Sources/AsyncRunnableCommand/AsyncRunnableCommand.swift
- Add AsyncParsableCommand module to dependency graph and layer summary
- Document AsyncRunnable vs AsyncParsableCommand incompatibility
- Add check-overlay.swift to Build & Test section
- Update RootCommand description (AsyncParsableCommand → ParsableCommand)
- Move CLAUDE.md to .claude/CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
Tests/AsyncParsableCommandTests/AsyncRunnableTests.swift (1)

92-100: Add the main-thread reentry case here.

runsOffMain() only checks where the async body starts. The #152 regression was the next step: hopping from that off-main context back to the main thread. A small command in this suite that calls DispatchQueue.main.sync inside run() and asserts completion would cover the path Codecov still reports as uncovered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/AsyncParsableCommandTests/AsyncRunnableTests.swift` around lines 92 -
100, Add a new test in the ThreadBehavior suite that verifies reentry to the
main thread: create a small command (e.g., MainReentryCommand) whose
run()/execute() body dispatches back to the main queue using
DispatchQueue.main.sync and completes, then invoke it with the same test harness
(similar to runsOffMain using runSync or the async runner) and assert that
completion occurs on the main thread (check Witnesses.current.threadLabel ==
"com.apple.main-thread" or equivalent). Ensure the test name describes
main-thread reentry and place it alongside runsOffMain to exercise the
regression path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Scripts/check-overlay.swift`:
- Around line 13-19: The cast to [String: Int] for kCGWindowBounds fails because
the values are NSNumber/CGAFloat; update the extraction from lyraWindows to read
bounds as a [String: Any] (or CFDictionary) and convert the numeric values to
Ints (or use CGRect(dictionaryRepresentation:)) before deriving width/height;
specifically modify the code around lyraWindows and the kCGWindowBounds access
to either parse bounds as [String: Any] and coerce bounds["Width"]/["Height"] to
numbers and then Int(...) or obtain boundsDict = w[kCGWindowBounds as String] as
CFDictionary? and call CGRect(dictionaryRepresentation:) to get
rect.width/rect.height and then Int(rect.width)/Int(rect.height) for width and
height used later.

In `@Sources/AsyncParsableCommand/AsyncRunnable.swift`:
- Around line 29-43: The current mutating run() uses
UnsafeMutableTransferBox(self) named command to call await
command.wrappedValue.run() but never writes the boxed, potentially-mutated value
back into self; after semaphore.wait() and before evaluating
result.wrappedValue.get(), assign self = command.wrappedValue so any mutations
performed by command.wrappedValue.run() are propagated back to the original
value, then call try result.wrappedValue.get() to rethrow any error; reference
AsyncRunnable.run, command (UnsafeMutableTransferBox) and result.wrappedValue
when making this change.

---

Nitpick comments:
In `@Tests/AsyncParsableCommandTests/AsyncRunnableTests.swift`:
- Around line 92-100: Add a new test in the ThreadBehavior suite that verifies
reentry to the main thread: create a small command (e.g., MainReentryCommand)
whose run()/execute() body dispatches back to the main queue using
DispatchQueue.main.sync and completes, then invoke it with the same test harness
(similar to runsOffMain using runSync or the async runner) and assert that
completion occurs on the main thread (check Witnesses.current.threadLabel ==
"com.apple.main-thread" or equivalent). Ensure the test name describes
main-thread reentry and place it alongside runsOffMain to exercise the
regression path.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48f188f5-9ad9-404e-9f37-0b788d42d429

📥 Commits

Reviewing files that changed from the base of the PR and between 86b2f3f and 1e70a8d.

📒 Files selected for processing (8)
  • Package.swift
  • Scripts/check-overlay.swift
  • Sources/AsyncParsableCommand/AsyncRunnable.swift
  • Sources/CLI/Commands/HealthcheckCommand.swift
  • Sources/CLI/Commands/RootCommand.swift
  • Sources/CLI/Commands/TrackCommand.swift
  • Sources/Presenters/Wallpaper/WallpaperPresenter.swift
  • Tests/AsyncParsableCommandTests/AsyncRunnableTests.swift

Comment thread .claude/scripts/check-overlay.swift
Comment on lines +29 to +43
public mutating func run() throws {
let command = UnsafeMutableTransferBox(self)
let result = UnsafeMutableTransferBox(Result<Void, any Error>.success(()))
let semaphore = DispatchSemaphore(value: 0)
Task {
do {
try await command.wrappedValue.run()
result.wrappedValue = .success(())
} catch {
result.wrappedValue = .failure(error)
}
semaphore.signal()
}
semaphore.wait()
try result.wrappedValue.get()

@coderabbitai coderabbitai Bot Apr 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate mutations back to self before returning.

Line 30 boxes a copy of self, but Lines 42-43 never write the mutated value back. Any state changes made inside run() async throws are discarded, so this is not a faithful implementation of the mutating run() contract.

🛠️ Proposed fix
     Task {
         do {
             try await command.wrappedValue.run()
             result.wrappedValue = .success(())
         } catch {
             result.wrappedValue = .failure(error)
         }
         semaphore.signal()
     }
     semaphore.wait()
+    self = command.wrappedValue
     try result.wrappedValue.get()

Do the assignment before get() so mutations made before a thrown error are preserved too.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public mutating func run() throws {
let command = UnsafeMutableTransferBox(self)
let result = UnsafeMutableTransferBox(Result<Void, any Error>.success(()))
let semaphore = DispatchSemaphore(value: 0)
Task {
do {
try await command.wrappedValue.run()
result.wrappedValue = .success(())
} catch {
result.wrappedValue = .failure(error)
}
semaphore.signal()
}
semaphore.wait()
try result.wrappedValue.get()
public mutating func run() throws {
let command = UnsafeMutableTransferBox(self)
let result = UnsafeMutableTransferBox(Result<Void, any Error>.success(()))
let semaphore = DispatchSemaphore(value: 0)
Task {
do {
try await command.wrappedValue.run()
result.wrappedValue = .success(())
} catch {
result.wrappedValue = .failure(error)
}
semaphore.signal()
}
semaphore.wait()
self = command.wrappedValue
try result.wrappedValue.get()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AsyncParsableCommand/AsyncRunnable.swift` around lines 29 - 43, The
current mutating run() uses UnsafeMutableTransferBox(self) named command to call
await command.wrappedValue.run() but never writes the boxed, potentially-mutated
value back into self; after semaphore.wait() and before evaluating
result.wrappedValue.get(), assign self = command.wrappedValue so any mutations
performed by command.wrappedValue.run() are propagated back to the original
value, then call try result.wrappedValue.get() to rethrow any error; reference
AsyncRunnable.run, command (UnsafeMutableTransferBox) and result.wrappedValue
when making this change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional. The actual commands (TrackCommand, HealthcheckCommand) communicate via stdout and exit codes, not struct mutation. This is specified in AsyncRunnableCommandTests.swift → "sync run() calls async run() and completes" verifies side effects via an actor witness, not mutation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeneralD, understood — since all conforming commands express results through stdout and ExitCode rather than struct field mutations, propagating command.wrappedValue back to self is unnecessary. The actor-based witness pattern in the tests is a clear specification of this contract. I'll note this for future reviews.


✏️ Learnings added
Learnt from: GeneralD
Repo: GeneralD/lyra PR: 153
File: Sources/AsyncParsableCommand/AsyncRunnableCommand.swift:29-43
Timestamp: 2026-04-04T15:35:36.749Z
Learning: In `Sources/AsyncParsableCommand/AsyncRunnableCommand.swift` (GeneralD/lyra), the `mutating run() throws` bridge intentionally does NOT write the boxed `command.wrappedValue` back to `self` after `semaphore.wait()`. All conforming commands (`TrackCommand`, `HealthcheckCommand`) communicate results via stdout and `ExitCode`, not via struct mutation. Do not flag the missing `self = command.wrappedValue` assignment as a bug; side effects are verified via an actor witness in tests, not by inspecting mutated struct fields.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: GeneralD
Repo: GeneralD/lyra PR: 149
File: Sources/CLI/Commands/TrackCommand.swift:54-59
Timestamp: 2026-04-01T14:09:10.078Z
Learning: In `Sources/CLI/Commands/TrackCommand.swift` (GeneralD/lyra), the `resolvedMetadata(track:)` helper intentionally runs `metadataUseCase.resolveCandidates(track:)` when either `--resolve` OR `--lyrics` is set. This is because `resolveCandidates` strips noise words and brackets (e.g. `"【MV】Song Name / Artist - Official Video"`) and generates title/artist permutations that LRCLIB requires for accurate lyrics matching. Metadata resolution is a prerequisite for effective lyrics lookup, not an independent concern. Do not flag `guard resolve || lyrics` as incorrect coupling.

Learnt from: GeneralD
Repo: GeneralD/lyra PR: 145
File: Sources/WallpaperDataSource/FindExecutable.swift:3-13
Timestamp: 2026-03-28T00:56:53.421Z
Learning: In the Lyra Swift codebase, `ObjCBool` is intentionally avoided. During code review, do not recommend or introduce APIs that require an `ObjCBool` out-parameter; instead, prefer pure-Swift alternatives and Swift-first APIs. For example, when checking file executability, avoid suggesting `FileManager.fileExists(atPath:isDirectory:)` (which uses an `ObjCBool`) as a replacement for `isExecutableFile(atPath:)`.

GeneralD and others added 2 commits April 5, 2026 00:02
Aligns with ParsableCommand / AsyncParsableCommand naming convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
Sources/AsyncParsableCommand/AsyncRunnableCommand.swift (1)

29-43: ⚠️ Potential issue | 🟠 Major

Write the mutated boxed command back to self before rethrowing.

UnsafeMutableTransferBox(self) boxes a copy. Any state changes made inside run() async throws stay in command.wrappedValue; without copying that value back after semaphore.wait(), the sync bridge drops those mutations and breaks the mutating contract.

🛠️ Proposed fix
         }
         semaphore.wait()
+        self = command.wrappedValue
         try result.wrappedValue.get()

This checks the bridge implementation directly. Expected result: the sed output shows let command = UnsafeMutableTransferBox(self), and the rg search finds no self = command.wrappedValue write-back.

#!/bin/bash
sed -n '29,44p' Sources/AsyncParsableCommand/AsyncRunnableCommand.swift
printf '\n-- write-back search --\n'
rg -n 'self\s*=\s*command\.wrappedValue' Sources/AsyncParsableCommand/AsyncRunnableCommand.swift
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AsyncParsableCommand/AsyncRunnableCommand.swift` around lines 29 -
43, The boxed `command` (created via UnsafeMutableTransferBox(self)) holds a
mutated copy from the async Task but you never write it back into self; after
semaphore.wait() and before rethrowing the error you must assign the mutated
value back (self = command.wrappedValue) so the mutating contract of run() is
preserved—insert that write-back immediately after semaphore.wait() and before
calling try result.wrappedValue.get().
.claude/scripts/check-overlay.swift (1)

15-17: ⚠️ Potential issue | 🟠 Major

Parse kCGWindowBounds as a Core Graphics dictionary.

CGWindowListCopyWindowInfo does not hand back kCGWindowBounds as [String: Int]. With the current cast, bounds falls back to [:], so width/height stay 0 and the script can fail even when the overlay is present.

🛠️ Proposed fix
 for w in lyraWindows {
     let pid = w["kCGWindowOwnerPID"] as? Int ?? 0
-    let bounds = w["kCGWindowBounds"] as? [String: Int] ?? [:]
-    let width = bounds["Width"] ?? 0
-    let height = bounds["Height"] ?? 0
+    let boundsDict = w[kCGWindowBounds as String] as CFDictionary?
+    let rect = boundsDict.flatMap { CGRect(dictionaryRepresentation: $0) } ?? .zero
+    let width = Int(rect.width)
+    let height = Int(rect.height)
     let memory = w["kCGWindowMemoryUsage"] as? Int ?? 0
     let onscreen = w["kCGWindowIsOnscreen"] as? Bool ?? false
What type does the `kCGWindowBounds` entry returned by `CGWindowListCopyWindowInfo` contain, and what is the recommended Swift way to convert it to a `CGRect`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/scripts/check-overlay.swift around lines 15 - 17, The issue is that
kCGWindowBounds is not a [String: Int] but an NSDictionary/CFDictionary with
NSNumber values, so the current cast yields an empty dict and zero sizes; change
the cast to treat w["kCGWindowBounds"] as an NSDictionary/CFDictionary and
convert it to a CGRect using CGRect(dictionaryRepresentation: ...) (or extract
numeric values as NSNumbers/CGFloats) and then read rect.width/rect.height for
width/height; update the usages of bounds, width, and height accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/CLAUDE.md:
- Around line 167-168: The layer summary references a non‑existent protocol name
"AsyncRunnableCommandCommand"; update that table row to refer to the actual
protocol name "AsyncRunnableCommand" (the symbol declared in
AsyncRunnableCommand.swift) and adjust any backtick/markdown or link text so it
points to AsyncRunnableCommand instead of AsyncRunnableCommandCommand.

---

Duplicate comments:
In @.claude/scripts/check-overlay.swift:
- Around line 15-17: The issue is that kCGWindowBounds is not a [String: Int]
but an NSDictionary/CFDictionary with NSNumber values, so the current cast
yields an empty dict and zero sizes; change the cast to treat
w["kCGWindowBounds"] as an NSDictionary/CFDictionary and convert it to a CGRect
using CGRect(dictionaryRepresentation: ...) (or extract numeric values as
NSNumbers/CGFloats) and then read rect.width/rect.height for width/height;
update the usages of bounds, width, and height accordingly.

In `@Sources/AsyncParsableCommand/AsyncRunnableCommand.swift`:
- Around line 29-43: The boxed `command` (created via
UnsafeMutableTransferBox(self)) holds a mutated copy from the async Task but you
never write it back into self; after semaphore.wait() and before rethrowing the
error you must assign the mutated value back (self = command.wrappedValue) so
the mutating contract of run() is preserved—insert that write-back immediately
after semaphore.wait() and before calling try result.wrappedValue.get().
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea4dad3a-ed16-4757-a2e3-1737f7c84ad3

📥 Commits

Reviewing files that changed from the base of the PR and between 1e70a8d and f7918fa.

📒 Files selected for processing (6)
  • .claude/CLAUDE.md
  • .claude/scripts/check-overlay.swift
  • Sources/AsyncParsableCommand/AsyncRunnableCommand.swift
  • Sources/CLI/Commands/HealthcheckCommand.swift
  • Sources/CLI/Commands/TrackCommand.swift
  • Tests/AsyncParsableCommandTests/AsyncRunnableCommandTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/CLI/Commands/TrackCommand.swift
  • Sources/CLI/Commands/HealthcheckCommand.swift

Comment thread .claude/CLAUDE.md Outdated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@GeneralD GeneralD requested a review from Copilot April 4, 2026 15:37
Avoid confusion with ArgumentParser's built-in AsyncParsableCommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread Sources/AsyncRunnableCommand/AsyncRunnableCommand.swift
Comment thread Sources/Presenters/Wallpaper/WallpaperPresenter.swift
@gitar-bot

gitar-bot Bot commented Apr 4, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Fixes SIGTRAP crash and blank overlay caused by AsyncParsableCommand by resolving DispatchQueue.main.sync deadlocks on the main thread and eliminating unnecessary task spawning in periodic observer callbacks. No remaining issues.

✅ 4 resolved
Edge Case: DispatchQueue.main.sync deadlocks if called on main thread

📄 Sources/CLI/Commands/DaemonCommand.swift:14
DispatchQueue.main.sync will deadlock if run() is ever called from the main thread (e.g. direct CLI invocation where ArgumentParser dispatches on main). While the PR description notes this fixes the LaunchAgent case (non-main thread), it introduces a regression risk for the direct-CLI case.

Consider guarding against this by checking the current thread:

func run() {
    let block = {
        let app = NSApplication.shared
        app.setActivationPolicy(.accessory)
        let delegate = AppDelegate()
        app.delegate = delegate
        _ = delegate
        app.run()
    }
    if Thread.isMainThread {
        block()
    } else {
        DispatchQueue.main.sync { block() }
    }
}

This safely handles both launch paths.

Performance: Task spawned on every periodic observer callback even when unneeded

📄 Sources/Presenters/Wallpaper/WallpaperPresenter.swift:69-70
Moving the time >= seekEnd guard inside the Task { @MainActor in } block means a new Task is allocated and enqueued on every 0.1s periodic time observer callback, even when the current time hasn't reached seekEnd (which is most of playback). Previously the check was synchronous in the callback, avoiding the Task allocation entirely until the seek point was reached.

At 10 callbacks/second this is negligible overhead, but the original pattern was more efficient and the semantic meaning is the same since the callback queue is already .main.

Edge Case: AsyncRunnable deadlocks if async run() touches @mainactor

📄 Sources/AsyncParsableCommand/AsyncRunnable.swift:29-43
The sync run() bridge in AsyncRunnable blocks the calling thread (typically main) with DispatchSemaphore.wait() while a Task executes the async run(). If any code reachable from the async run() dispatches to @MainActor, the semaphore will never be signalled because the main thread is blocked — classic deadlock.

Current commands (HealthcheckCommand, TrackCommand) don't hit @MainActor, so this is safe today. However, a future contributor adding @MainActor-isolated code in a dependency would trigger a silent deadlock with no diagnostic. A doc comment or dispatchPrecondition would make this constraint explicit.

Edge Case: Struct mutations in async run() are silently lost

📄 Sources/AsyncRunnableCommand/AsyncRunnableCommand.swift:29-43
The sync run() bridge copies self into an UnsafeMutableTransferBox, then the async run() operates on that copy. After semaphore.wait() completes, the original self is never updated from command.wrappedValue. If a future AsyncRunnableCommand conformer mutates self inside its async run() (e.g. setting a property for post-run reporting), those mutations are silently discarded.

This is unlikely to bite current conformers (HealthcheckCommand, TrackCommand) since they don't mutate self, but it's a footgun for future adopters — especially since the protocol declares mutating func run() async throws, implying mutation is supported.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +29 to +44
public mutating func run() throws {
let command = UnsafeMutableTransferBox(self)
let result = UnsafeMutableTransferBox(Result<Void, any Error>.success(()))
let semaphore = DispatchSemaphore(value: 0)
Task {
do {
try await command.wrappedValue.run()
result.wrappedValue = .success(())
} catch {
result.wrappedValue = .failure(error)
}
semaphore.signal()
}
semaphore.wait()
try result.wrappedValue.get()
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bridge can deadlock when run() is invoked from a MainActor-bound context: Task { ... } inherits the current actor, so if it inherits MainActor it may only execute on the main thread while semaphore.wait() blocks that same thread. To make this safe, ensure the async work does not inherit the caller actor (e.g., use a detached task or an explicit executor hop) and avoid blocking the main thread indefinitely.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the gitar-bot thread above. Commands using AsyncRunnableCommand do not touch @mainactor.

Comment on lines +29 to +30
public mutating func run() throws {
let command = UnsafeMutableTransferBox(self)

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because self is copied into command (UnsafeMutableTransferBox(self)), any mutations performed inside mutating func run() async throws are applied to the copy and will be lost when the sync run() returns. If callers expect run() mutations to persist (consistent with ParsableCommand semantics), capture a mutable local copy, run async work against it, then assign it back to self after the wait completes.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By design. See coderabbitai thread.

let semaphore = DispatchSemaphore(value: 0)
Task {
do {
try await command.wrappedValue.run()

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because self is copied into command (UnsafeMutableTransferBox(self)), any mutations performed inside mutating func run() async throws are applied to the copy and will be lost when the sync run() returns. If callers expect run() mutations to persist (consistent with ParsableCommand semantics), capture a mutable local copy, run async work against it, then assign it back to self after the wait completes.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate of above.

Comment on lines 66 to 72
if let seekEnd {
let interval = CMTime(seconds: 0.1, preferredTimescale: 600)
endTimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self, weak player] time in
guard time >= seekEnd else { return }
Task { @MainActor in
guard let self, !self.isSeeking else { return }
guard let self, !self.isSeeking, time >= seekEnd else { return }
self.isSeeking = true
player?.seek(to: seekStart, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving time >= seekEnd inside the Task means a new Task is created on every periodic tick (0.1s) even when time is still below seekEnd. This adds avoidable overhead on the main queue. Consider restoring an outer early-return guard (before creating the Task) and keep the isSeeking guard inside the @MainActor scope if needed.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as gitar-bot thread. This is the original v2.5.5 behavior restored after #154.

Comment on lines +5 to +6
let windows = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]] ?? []
let lyraWindows = windows.filter { ($0["kCGWindowOwnerName"] as? String) == "lyra" }

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window info dictionaries are keyed by CoreGraphics constants (CFStrings). Using string literals like \"kCGWindowOwnerName\" is brittle and can break if the underlying keys don’t match the literal exactly. Prefer using the constants (kCGWindowOwnerName, kCGWindowOwnerPID, etc., typically bridged as as String) when accessing the dictionary.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CGWindowListCopyWindowInfo returns CFDictionary bridged to [String: Any]. The keys are C string constants (kCGWindowOwnerName etc.) which are always these exact string values. This is the standard Swift usage pattern.

Comment on lines +14 to +19
let pid = w["kCGWindowOwnerPID"] as? Int ?? 0
let bounds = w["kCGWindowBounds"] as? [String: Int] ?? [:]
let width = bounds["Width"] ?? 0
let height = bounds["Height"] ?? 0
let memory = w["kCGWindowMemoryUsage"] as? Int ?? 0
let onscreen = w["kCGWindowIsOnscreen"] as? Bool ?? false

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window info dictionaries are keyed by CoreGraphics constants (CFStrings). Using string literals like \"kCGWindowOwnerName\" is brittle and can break if the underlying keys don’t match the literal exactly. Prefer using the constants (kCGWindowOwnerName, kCGWindowOwnerPID, etc., typically bridged as as String) when accessing the dictionary.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment on lines +94 to +100
@Test("async run executes off the main dispatch queue")
func runsOffMain() async throws {
Witnesses.current = Witness()
var command = ThreadCheckCommand()
try runSync(&command)
#expect(await Witnesses.current.threadLabel != "com.apple.main-thread")
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s assertion relies on dispatch queue labels (__dispatch_queue_get_label(nil)), which can be empty or non-deterministic for Swift concurrency executors and may lead to flaky results across OS/toolchain versions. A more robust signal is Thread.isMainThread (store a Bool witness), or explicitly control the executor used by the bridge and assert against that.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test asserts the label is NOT com.apple.main-thread. Swift cooperative pool threads have deterministic labels (e.g. com.apple.root.default-qos.cooperative). This has been stable across all CI runs.

Comment on lines +171 to +174
mutating func run() async throws {
let label = String(cString: __dispatch_queue_get_label(nil))
await Witnesses.current.setThread(label)
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test’s assertion relies on dispatch queue labels (__dispatch_queue_get_label(nil)), which can be empty or non-deterministic for Swift concurrency executors and may lead to flaky results across OS/toolchain versions. A more robust signal is Thread.isMainThread (store a Bool witness), or explicitly control the executor used by the bridge and assert against that.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate of above.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift`:
- Around line 11-15: The test currently infers execution context via the
threadLabel variable and setThread(_:) helper—replace that indirect dispatch
queue label check with an explicit Thread.isMainThread assertion to make the
intent clear; locate uses of threadLabel, setThread(_:) and any assertions
comparing queue labels in AsyncRunnableCommandTests (and remove or stop setting
threadLabel in markCalled()/set(_:)/setThread(_:)), then assert
Thread.isMainThread (or its negation as appropriate) where the test previously
checked the dispatch queue label and update the test name to reflect
"main-thread" behavior if present.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68a62080-4033-4181-a7f0-a15218549a54

📥 Commits

Reviewing files that changed from the base of the PR and between 699370d and 898c9f2.

📒 Files selected for processing (6)
  • .claude/CLAUDE.md
  • Package.swift
  • Sources/AsyncRunnableCommand/AsyncRunnableCommand.swift
  • Sources/CLI/Commands/HealthcheckCommand.swift
  • Sources/CLI/Commands/TrackCommand.swift
  • Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/CLI/Commands/HealthcheckCommand.swift
  • Package.swift
  • Sources/CLI/Commands/TrackCommand.swift
  • .claude/CLAUDE.md

Comment on lines +11 to +15
var threadLabel = ""

func markCalled() { called = true }
func set(_ v: Int) { value = v }
func setThread(_ label: String) { threadLabel = label }

@coderabbitai coderabbitai Bot Apr 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Swift Testing, are asynctest functions guaranteed to run on the main thread by default, and isThread.isMainThreadpreferred over__dispatch_queue_get_label(nil) when asserting whether code executed on the main thread?

💡 Result:

No, async test functions in Swift Testing are not guaranteed to run on the main thread by default. They execute on arbitrary actors unless explicitly isolated to @MainActor or another actor. Official documentation states that test functions can be marked async and throws, and if needing main actor context, specify @MainActor explicitly (e.g., @Test @MainActor func ...). XCTest async tests without @MainActor run on arbitrary actors, but Swift Testing follows similar concurrency integration without main thread guarantee by default. For asserting main thread execution, Thread.isMainThread is preferred over dispatch_queue_get_label(nil). Thread.isMainThread is the standard, high-level Swift API for checking the main thread, commonly recommended in documentation and community resources. Queue label checks are lower-level, used for specific DispatchQueue verification (e.g., via dispatchPrecondition), but for main thread specifically, Thread.isMainThread is simpler, more reliable, and idiomatic. Sources confirm its use in tests and concurrency contexts without issues.

Citations:


🏁 Script executed:

cd /tmp && find . -name "AsyncRunnableCommandTests.swift" -type f 2>/dev/null | head -5

Repository: GeneralD/lyra

Length of output: 39


🏁 Script executed:

find . -path "*/Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift" -type f

Repository: GeneralD/lyra

Length of output: 122


🏁 Script executed:

git ls-files | grep -i "asyncrunnablecommandtests"

Repository: GeneralD/lyra

Length of output: 120


🏁 Script executed:

wc -l Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift

Repository: GeneralD/lyra

Length of output: 124


🏁 Script executed:

cat -n Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift | head -200

Repository: GeneralD/lyra

Length of output: 6581


Replace the dispatch queue label check with Thread.isMainThread for clearer, more idiomatic thread assertions.

Issue #152 targets a main-thread caller bug, but this test currently infers context indirectly via dispatch queue label—an implementation detail that's less reliable and harder to read than the standard API. The test name also doesn't match the actual invariant (issue is about main-thread callers, not queue labels). Using Thread.isMainThread makes both the test intent and the regression guard explicit and idiomatic.

🧪 Suggested changes
 private actor Witness {
     var called = false
     var value = 0
-    var threadLabel = ""
+    var ranOnMainThread = false

     func markCalled() { called = true }
     func set(_ v: Int) { value = v }
-    func setThread(_ label: String) { threadLabel = label }
+    func setRanOnMainThread(_ value: Bool) { ranOnMainThread = value }
 }
@@
         `@Test`("async run executes off the main dispatch queue")
         func runsOffMain() async throws {
             Witnesses.current = Witness()
             var command = ThreadCheckCommand()
             try runSync(&command)
-            `#expect`(await Witnesses.current.threadLabel != "com.apple.main-thread")
+            `#expect`(await Witnesses.current.ranOnMainThread == false)
         }
@@
     mutating func run() async throws {
-        let label = String(cString: __dispatch_queue_get_label(nil))
-        await Witnesses.current.setThread(label)
+        await Witnesses.current.setRanOnMainThread(Thread.isMainThread)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/AsyncRunnableCommandTests/AsyncRunnableCommandTests.swift` around lines
11 - 15, The test currently infers execution context via the threadLabel
variable and setThread(_:) helper—replace that indirect dispatch queue label
check with an explicit Thread.isMainThread assertion to make the intent clear;
locate uses of threadLabel, setThread(_:) and any assertions comparing queue
labels in AsyncRunnableCommandTests (and remove or stop setting threadLabel in
markCalled()/set(_:)/setThread(_:)), then assert Thread.isMainThread (or its
negation as appropriate) where the test previously checked the dispatch queue
label and update the test name to reflect "main-thread" behavior if present.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suite is .serialized and each test resets Witnesses.current before use. There is no concurrent access. nonisolated(unsafe) is correct here as the serialization trait guarantees sequential execution.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeneralD, that's a fair point — nonisolated(unsafe) paired with the .serialized suite trait is correct, and I agree there's no concurrent access concern there.

My suggestion was actually about a separate issue: the mechanism used to detect the main thread inside ThreadCheckCommand.run(). The current code reads:

let label = String(cString: __dispatch_queue_get_label(nil))
await Witnesses.current.setThread(label)
// ...
`#expect`(await Witnesses.current.threadLabel != "com.apple.main-thread")

__dispatch_queue_get_label(nil) is a low-level C function that returns the label of the current queue, which isn't quite the same as checking the main thread — a thread can run work from the main queue and then be re-used for other queues, and vice versa. It also makes the assertion string "com.apple.main-thread" a private implementation detail that could silently break. Thread.isMainThread is the standard, high-level Swift API explicitly designed for this check and maps directly to the invariant the test is guarding (that the async bridge does not execute on the main thread). The change would be minimal:

// In Witness actor
var ranOnMainThread = false
func setRanOnMainThread(_ v: Bool) { ranOnMainThread = v }

// In ThreadCheckCommand.run()
await Witnesses.current.setRanOnMainThread(Thread.isMainThread)

// In the test
`#expect`(await Witnesses.current.ranOnMainThread == false)

This is purely about correctness and clarity of the thread-detection mechanism, not about nonisolated(unsafe) or concurrent access. Happy to defer if you prefer keeping the queue-label approach, but I wanted to make the distinction clear.

(ↀᴥↀ)


🧠 Learnings used
Learnt from: GeneralD
Repo: GeneralD/lyra PR: 153
File: Sources/AsyncParsableCommand/AsyncRunnableCommand.swift:29-43
Timestamp: 2026-04-04T15:35:36.749Z
Learning: In `Sources/AsyncParsableCommand/AsyncRunnableCommand.swift` (GeneralD/lyra), the `mutating run() throws` bridge intentionally does NOT write the boxed `command.wrappedValue` back to `self` after `semaphore.wait()`. All conforming commands (`TrackCommand`, `HealthcheckCommand`) communicate results via stdout and `ExitCode`, not via struct mutation. Do not flag the missing `self = command.wrappedValue` assignment as a bug; side effects are verified via an actor witness in tests, not by inspecting mutated struct fields.

Learnt from: GeneralD
Repo: GeneralD/lyra PR: 149
File: Sources/CLI/Commands/TrackCommand.swift:54-59
Timestamp: 2026-04-01T14:09:10.078Z
Learning: In `Sources/CLI/Commands/TrackCommand.swift` (GeneralD/lyra), the `resolvedMetadata(track:)` helper intentionally runs `metadataUseCase.resolveCandidates(track:)` when either `--resolve` OR `--lyrics` is set. This is because `resolveCandidates` strips noise words and brackets (e.g. `"【MV】Song Name / Artist - Official Video"`) and generates title/artist permutations that LRCLIB requires for accurate lyrics matching. Metadata resolution is a prerequisite for effective lyrics lookup, not an independent concern. Do not flag `guard resolve || lyrics` as incorrect coupling.

Learnt from: GeneralD
Repo: GeneralD/lyra PR: 145
File: Sources/WallpaperDataSource/FindExecutable.swift:3-13
Timestamp: 2026-03-28T00:56:53.421Z
Learning: In the Lyra Swift codebase, `ObjCBool` is intentionally avoided. During code review, do not recommend or introduce APIs that require an `ObjCBool` out-parameter; instead, prefer pure-Swift alternatives and Swift-first APIs. For example, when checking file executability, avoid suggesting `FileManager.fileExists(atPath:isDirectory:)` (which uses an `ObjCBool`) as a replacement for `isExecutableFile(atPath:)`.

@GeneralD GeneralD merged commit 427f96a into main Apr 4, 2026
4 checks passed
@GeneralD GeneralD deleted the fix/152-daemon-main-thread branch April 4, 2026 15:52
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.

bug: wallpaper not displayed after seekEnd guard optimization in v2.5.6 bug: DaemonCommand crashes with SIGTRAP when launched via LaunchAgent

3 participants