Skip to content

feat(desktop): 通知音と Aivis 音声の重なりを防ぐ AudioScheduler 導入#405

Merged
MocA-Love merged 5 commits intomainfrom
feat/desktop-audio-scheduler
Apr 24, 2026
Merged

feat(desktop): 通知音と Aivis 音声の重なりを防ぐ AudioScheduler 導入#405
MocA-Love merged 5 commits intomainfrom
feat/desktop-audio-scheduler

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 23, 2026

Summary

通知音(ringtone)と Aivis 音声通知が同時イベントで重なって再生されてしまう問題を解消します。

変更の要点

  • 通知音は "ドロップ": ringtone / Aivis のいずれかが再生中なら、新しく発生した通知音は無視する。通知音は情報量がないので捨てて構わない。
  • Aivis は "FIFO キュー": 発話中のキューに積んで直列再生。ワークスペース名や branch を読み上げるため、捨てず順番に喋らせる。
  • PermissionRequest は優先: 進行中の発話は打ち切らないが、Stop イベントより先にキューの前に割り込ませる。
  • Aivis API エラーを分類:
    • Retryable(429 / 5xx / network / timeout): 指数バックオフで最大 3 回リトライ。429X-Aivis-RateLimit-Requests-Reset ヘッダーの秒数 + 0.5s を優先。
    • Fatal(401 / 402 / 404): キューを破棄して一時停止。原因を OS 通知で 1 回だけ表示(1 分クールダウン)。ユーザー対応待ちなので自動再開はしない。
    • Item-specific(422): そのアイテムだけスキップしてキュー継続。
  • Proactive レート制限: 成功レスポンスの X-Aivis-RateLimit-Requests-Remaining / -Reset を記録し、Remaining=0 なら次の送信を Reset + 0.5s 待機して先回りで回避。
  • タイムアウト: /v1/tts/synthesize に 30s の AbortSignal を追加(通常 1 秒未満で返る API なのに永久 hang しないように)。

新規ファイル

  • apps/desktop/src/main/lib/notifications/audio-scheduler.ts — ドロップ / キュー / 再試行 / 停止ロジックを集約
  • apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts — スケジューラー単体テスト(13 ケース)

既存ファイルの変更

  • aivis-tts.ts: synthesizeAivisAudio / playAivisAudio / buildAivisTaskRunner に分離。AivisError を throw するよう再実装。設定画面の「テスト発声」用 playAivisTts はそのまま維持(スケジューラーをバイパスする一発実行)。
  • notification-manager.ts: playSound / playAivis の依存を playRingtone / buildAivisRunner / enqueueAivis に置き換え。
  • main.ts: AudioScheduler を初期化して NotificationManager に配線。致命的エラー時の OS 通知ハンドラ showAivisPausedNotification を追加。

設計の補足

  • 設定画面の ringtone プレビューと Aivis テスト発声は別チャネル扱いで、スケジューラーをバイパスします(プレビュー中でも本番通知は普通に鳴る)。
  • dispose() でキューを破棄するので、ウィンドウ閉鎖時に未処理タスクが残ることはありません。
  • Aivis paused OS 通知は 1 分クールダウン。致命的エラーが連発しても OS 通知を爆撃しません。

Test plan

  • bun test src/main/lib/notifications/ — 87 pass / 0 fail(うち audio-scheduler 単独で 13 ケース)
  • bun run typecheck — 通過
  • bun run lint — 通過
  • 配布ビルドで実機確認(Stop + PermissionRequest を同時に発火させて重ならないこと)

Summary by CodeRabbit

リリースノート

  • 新機能

    • 通知音の重複再生を防止し、他のオーディオ再生中は通知音を自動的にキューイング
    • Permission Request通知に優先処理を実装
    • Aivis API レート制限への対応を強化(リセット時間の自動管理)
  • テスト

    • オーディオスケジューリング機能の包括的なテストカバレッジを追加

通知音と Aivis TTS が同時イベントで重なる問題を解消。

- 通知音: ringtone / aivis いずれかが再生中なら 2 件目を破棄
- Aivis: FIFO キューで直列再生、PermissionRequest は Stop より優先
- Aivis API エラーをステータスごとに分類:
  - 429 / 5xx / network: 指数バックオフで最大 3 回リトライ。429 は
    X-Aivis-RateLimit-Requests-Reset ヘッダーを尊重
  - 401 / 402 / 404: キューを破棄して一時停止し、OS 通知で理由を表示
  - 422: そのアイテムのみスキップ
- 成功レスポンスの RateLimit ヘッダーを記録し、Remaining 0 の時は
  Reset + 0.5s 待機する proactive 制御
- 30s のリクエストタイムアウトを追加
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 34 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2abd8d6-3bb5-4ef6-a0c8-17744f3234b4

📥 Commits

Reviewing files that changed from the base of the PR and between 42b5276 and e27f646.

📒 Files selected for processing (3)
  • apps/desktop/src/main/lib/notifications/aivis-tts.ts
  • apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts
  • apps/desktop/src/main/lib/notifications/audio-scheduler.ts
📝 Walkthrough

Walkthrough

AudioSchedulerを導入して通知音声再生を調整し、リングトーンとAivis TTSの重複を防止。Aivis TTSタスクを優先度付きキューで処理し、レート制限をヘッダーから取得して管理。aivis-ttsモジュールを合成と再生の関心分離に再設計。

Changes

Cohort / File(s) Summary
ドキュメント更新
README.md
音声再生重複防止機能の変更履歴を記録:通知音のドロップ、Aivis TTS順序実行、PermissionRequestプリエンプション、Aivis APIエラー処理。
オーディオスケジューラ実装
apps/desktop/src/main/lib/notifications/audio-scheduler.ts
リングトーンとAivis TTSを調整する新しいAudioSchedulerクラス:FIFO/優先度キュー、レート制限追跡、指数バックオフ再試行(最大3回)、エラーハンドリング、状態管理。
スケジューラテスト
apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts
AudioSchedulerの包括的なテストスイート:リングトーン調整、優先度キュー、再試行ロジック、429レート制限処理、致命的エラー時のポーズ、処分処理。
Aivis TTS再設計
apps/desktop/src/main/lib/notifications/aivis-tts.ts
合成と再生を分離:synthesizeAivisAudio()で30秒タイムアウト・ステータス分類・ヘッダー取得、playAivisAudio()で一時ファイル処理、buildAivisTaskRunner()でスケジューラ統合。
通知マネージャ統合
apps/desktop/src/main/lib/notifications/notification-manager.ts, notification-manager.test.ts
リングトーン再生と独立したAivis タスク生成:playRingtone()buildAivisRunner()/enqueueAivis()で分離。イベント種別に基づく優先度(PermissionRequest=高、その他=通常)。
メインウィンドウ統合
apps/desktop/src/main/windows/main.ts
AudioScheduler の配線:playRingtone/buildAivisRunner/enqueueAivisフック、showAivisPausedNotification(1分クールダウン)、クリーンアップ時の処分。

Sequence Diagram

sequenceDiagram
    participant User
    participant NotificationManager
    participant AudioScheduler
    participant AivisAPI
    participant Ringtone
    participant Speaker

    User->>NotificationManager: Lifecycle Event (Stop/PermissionRequest)
    NotificationManager->>AudioScheduler: playRingtone()
    
    alt Ringtone Idle
        AudioScheduler->>Ringtone: Play
        Ringtone->>Speaker: Audio Output
        Note over Ringtone: Playing...
    end
    
    NotificationManager->>NotificationManager: buildAivisRunner(event)
    NotificationManager->>AudioScheduler: enqueueAivis(runner, priority)
    Note over AudioScheduler: Task queued (FIFO/Priority)
    
    loop Rate Limit Check
        AudioScheduler->>AudioScheduler: Check remaining quota
        alt Quota Available
            AudioScheduler->>AudioScheduler: Proceed
        else Quota Exhausted
            AudioScheduler->>AudioScheduler: Await resetSeconds + margin
        end
    end
    
    Note over Ringtone: Completion
    AudioScheduler->>AudioScheduler: Mark ringtone idle
    
    par Aivis Processing
        AudioScheduler->>AivisAPI: synthesizeAivisAudio()
        AivisAPI-->>AudioScheduler: Audio Buffer + Rate-Limit Headers
        Note over AudioScheduler: Capture X-Aivis-RateLimit-*
        AudioScheduler->>AudioScheduler: playAivisAudio(buffer)
        AudioScheduler->>Speaker: Play Aivis Audio
    end
    
    alt Error (429/5xx)
        AudioScheduler->>AudioScheduler: Exponential backoff retry
    else Error (401/402/404)
        AudioScheduler->>AudioScheduler: Pause queue
        AudioScheduler->>User: Show "Aivis paused" notification
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 キュー並ぶ音、優先度で躍り出で
リングとAivisの重なり消えて
レート制限も賢く待ちて
スケジューラの妙なる調和
通知も心地よし〜♪

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% 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
Title check ✅ Passed タイトルは変更の主要な目的(AudioScheduler 導入による通知音と Aivis 音声の重なり防止)を明確かつ簡潔に表現している。
Description check ✅ Passed PR の説明は提供されているテンプレートの全セクション(Description、Related Issues、Type of Change、Testing、Additional Notes)をカバーしており、変更内容は詳細かつ明確である。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/desktop-audio-scheduler

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a8629aee7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +162 to +163
if (this.aivisBusy) return;
if (this.disposed || this.paused) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block Aivis pump while ringtone is still playing

The scheduler starts Aivis work as soon as an item is enqueued, but this guard only checks aivisBusy/paused and ignores ringtoneBusy. Since NotificationManager now calls playRingtone() and enqueueAivis() back-to-back for the same event, pump() can begin synthesis/play before the ringtone completion callback fires, so notification audio can overlap again on fast Aivis responses. This contradicts the stated non-overlap contract and is user-visible whenever ringtone duration exceeds synth latency.

Useful? React with 👍 / 👎.

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.

指摘の通りでした。pump() は aivisBusy しか見ておらず、ringtone 再生中でも Aivis の合成→再生が即座に始まる競合がありました。

修正: 合成はネットワーク呼び出しで音を出さないのでそのまま並行実行できるが、runner.play() の直前で waitForRingtoneIdle() を挟んで ringtone の onComplete を待つようにしました。ringtone が終わったら pending 待機者を全部 resolve します。dispose 時も待機者を flush して runOne が hang しないようにしています。

テスト2件追加 (ringtone 完了前は aivis play が呼ばれない / dispose で待機中の aivis が解放される)。

Codex レビュー (PR #405) で指摘されたバグ。pump() は aivisBusy のみを
チェックしていたので、playRingtone() 直後に enqueueAivis() を呼ぶと、
ringtone 再生中に Aivis の合成と再生が始まって音声が重なっていた。

修正: 合成はそのまま並行実行できる(ネットワーク呼び出しなので安全)
が、re-sampled した音声の再生 (runner.play) は waitForRingtoneIdle()
で ringtone 完了を待ってから実行するようにする。ringtone の
onComplete が pending 待機者を解放する。dispose 時も待機者を flush
して runOne が hang しないようにする。

テスト2件追加:
- ringtone 完了前は aivis play が呼ばれない
- dispose で待機中の aivis が解放される
@MocA-Love
Copy link
Copy Markdown
Owner Author

MocA-Love commented Apr 24, 2026

@codex review

前回指摘された ringtone 中の Aivis 再生ブロック問題を commit a4dfc1d で修正しました。runner.play() の直前に waitForRingtoneIdle() を挟んで ringtone 完了を待つ方式にしています。他にも気になる点があれば指摘してください。

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4dfc1dc75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private drainAndPause(reason: string): void {
const dropped = this.queue.length;
this.queue = [];
this.paused = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resume scheduler after fatal error recovery

drainAndPause permanently flips paused to true, and enqueueAivis drops all future tasks while paused, but this change set never wires any call to resume() from the main notification flow. As a result, a single 401/402/404 response disables Aivis for the rest of the running app session, even after the user fixes API key/credits/model settings, so subsequent voice notifications are silently lost until restart.

Useful? React with 👍 / 👎.

err,
);
}
clearTimeout(timeoutId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep synth timeout active through body read

The 30s timeout is cleared right after fetch() returns, before res.arrayBuffer() runs. Because fetch can resolve as soon as headers arrive, a slow or stalled response body can still hang this path well beyond 30s, leaving the scheduler busy and delaying later queued utterances despite the new timeout policy.

Useful? React with 👍 / 👎.

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.

ご指摘ありがとうございます。42b5276d で修正しました。

clearTimeout(timeoutId) を fetch() 直後ではなく try/finally の finally に移し、res.text() / res.arrayBuffer() の本体読み取りが完了 (or 例外) するまでタイマーを armed のまま保持するようにしました。本体読み取り中に AbortSignal で abort した場合も wrapFetchError で AivisError(retryable) に変換し、scheduler の retry ポリシーに乗せます。

Codex レビュー (PR #405) で指摘された P2 問題の修正。

fetch() はヘッダー到着時点で resolve するので、直後に clearTimeout
していると、サーバが body のストリームで stall した場合にタイムアウ
トが効かず、scheduler が 30s を大きく超えて busy のままになる恐れが
あった。

修正: timeoutId のクリアを try/finally の finally に移し、レスポンス
ボディ (res.text / res.arrayBuffer) の読み取り完了後に初めてタイマー
を止めるようにする。body 読み取り中の AbortError も wrapFetchError
経由で AivisError(retryable) に変換し、scheduler の retry ポリシー
に乗せる。
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/desktop/src/main/lib/notifications/aivis-tts.ts (1)

243-247: rm サブプロセスでの削除は fs/promisesunlink に置き換えるのが望ましい

execFile("rm", ...) は Windows で rm が存在しないため失敗し、コールバックでエラーを握りつぶしているのでテンポラリファイルがリークします。また本ファイルは既に node:fs/promises から writeFile を import しているので、サブプロセスを起動する必要はありません。

♻️ 提案パッチ
-import { writeFile } from "node:fs/promises";
+import { unlink, writeFile } from "node:fs/promises";
-function removeFile(path: string): void {
-	execFile("rm", ["-f", path], () => {
-		/* ignore */
-	});
-}
+function removeFile(path: string): void {
+	void unlink(path).catch(() => {
+		/* ignore — temp file may already be gone */
+	});
+}

execFile / child_process が他に使われていなければ import も整理できます。

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

In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts` around lines 243 - 247,
removeFile currently spawns rm via execFile which fails on Windows and swallows
errors; replace its implementation to use node:fs/promises.unlink (make
removeFile async or return the Promise) and properly handle ENOENT (ignore if
file already missing) while rethrowing other errors; update references to await
or handle the returned Promise, remove the child_process/execFile import if no
longer used, and keep existing writeFile import intact.
apps/desktop/src/main/lib/notifications/audio-scheduler.ts (2)

85-86: DEFAULT_BACKOFF_MS の 3 番目の要素は到達しない

MAX_RETRY_ATTEMPTS = 3 かつ Line 238 で attempt >= 3 のとき break するため、sleep() に渡されるのは attempt = 1, 2 の時のみ、すなわち DEFAULT_BACKOFF_MS[0][1] だけです。3 番目の 4000 は常にデッドコードになっています。

意図的に将来の MAX_RETRY_ATTEMPTS 引き上げに備えた配列であればコメントで明示、そうでなければ要素を 2 個に削減するか、配列を [1000, 2000] にして at(-1) フォールバック(Line 255)に一本化すると読み手の混乱を減らせます。

Also applies to: 238-240, 251-256

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

In `@apps/desktop/src/main/lib/notifications/audio-scheduler.ts` around lines 85 -
86, DEFAULT_BACKOFF_MS contains three values but MAX_RETRY_ATTEMPTS is 3, so the
third value (4000) is never used; update the retry/backoff logic by either
reducing DEFAULT_BACKOFF_MS to two entries ([1000, 2000]) or keep the array but
rely consistently on the fallback (e.g., using DEFAULT_BACKOFF_MS.at(-1)) where
backoff is selected in the retry loop (refer to MAX_RETRY_ATTEMPTS,
DEFAULT_BACKOFF_MS, and the sleep/backoff usage around the retry logic) and add
a short comment explaining the chosen approach so the dead-code confusion is
removed.

33-46: Error.causesuper のオプションで渡すのが簡潔

プロジェクトの TypeScript ターゲットは esnext に設定されているため、ES2022 の Error コンストラクタで cause オプションが利用可能です。手動キャストを避けて次のように書けます。

♻️ 提案パッチ
 export class AivisError extends Error {
 	constructor(
 		readonly kind: AivisErrorKind,
 		readonly reason: string,
 		readonly status?: number,
 		/** For 429: seconds to wait before retrying (from header). */
 		readonly rateLimitReset?: number,
 		cause?: unknown,
 	) {
-		super(reason);
+		super(reason, cause !== undefined ? { cause } : undefined);
 		this.name = "AivisError";
-		if (cause !== undefined) (this as { cause?: unknown }).cause = cause;
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/notifications/audio-scheduler.ts` around lines 33 -
46, The AivisError constructor currently assigns the cause via a manual cast
after calling super; instead pass the cause into Error's constructor options to
leverage ES2022's Error.cause support: in the AivisError constructor (class
AivisError) call super(reason, { cause }) when cause is provided (or
unconditionally pass the cause value), and remove the manual cast and assignment
of (this as { cause?: unknown }).cause = cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/lib/notifications/audio-scheduler.ts`:
- Around line 109-121: playRingtone currently relies on deps.playRingtone to
always call the provided callback, but if that contract is violated ringtoneBusy
may remain true and waitForRingtoneIdle will hang; update playRingtone and/or
waitForRingtoneIdle to defend against this by adding a bounded timeout and
explicit logging: wrap the deps.playRingtone invocation so that if
onRingtoneComplete is not invoked within N ms you force-clear ringtoneBusy via
onRingtoneComplete (or a shared cleanup path), emit a warning/error log that the
ringtone callback timed out, and ensure any finally-style cleanup always calls
onRingtoneComplete; reference the playRingtone, deps.playRingtone,
onRingtoneComplete, ringtoneBusy, and waitForRingtoneIdle symbols when making
the change.

---

Nitpick comments:
In `@apps/desktop/src/main/lib/notifications/aivis-tts.ts`:
- Around line 243-247: removeFile currently spawns rm via execFile which fails
on Windows and swallows errors; replace its implementation to use
node:fs/promises.unlink (make removeFile async or return the Promise) and
properly handle ENOENT (ignore if file already missing) while rethrowing other
errors; update references to await or handle the returned Promise, remove the
child_process/execFile import if no longer used, and keep existing writeFile
import intact.

In `@apps/desktop/src/main/lib/notifications/audio-scheduler.ts`:
- Around line 85-86: DEFAULT_BACKOFF_MS contains three values but
MAX_RETRY_ATTEMPTS is 3, so the third value (4000) is never used; update the
retry/backoff logic by either reducing DEFAULT_BACKOFF_MS to two entries ([1000,
2000]) or keep the array but rely consistently on the fallback (e.g., using
DEFAULT_BACKOFF_MS.at(-1)) where backoff is selected in the retry loop (refer to
MAX_RETRY_ATTEMPTS, DEFAULT_BACKOFF_MS, and the sleep/backoff usage around the
retry logic) and add a short comment explaining the chosen approach so the
dead-code confusion is removed.
- Around line 33-46: The AivisError constructor currently assigns the cause via
a manual cast after calling super; instead pass the cause into Error's
constructor options to leverage ES2022's Error.cause support: in the AivisError
constructor (class AivisError) call super(reason, { cause }) when cause is
provided (or unconditionally pass the cause value), and remove the manual cast
and assignment of (this as { cause?: unknown }).cause = cause.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 271a8c5b-ecf8-4c17-bda7-42b694928abc

📥 Commits

Reviewing files that changed from the base of the PR and between 99cbbec and 42b5276.

📒 Files selected for processing (7)
  • README.md
  • apps/desktop/src/main/lib/notifications/aivis-tts.ts
  • apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts
  • apps/desktop/src/main/lib/notifications/audio-scheduler.ts
  • apps/desktop/src/main/lib/notifications/notification-manager.test.ts
  • apps/desktop/src/main/lib/notifications/notification-manager.ts
  • apps/desktop/src/main/windows/main.ts

Comment thread apps/desktop/src/main/lib/notifications/audio-scheduler.ts
PR #405 の CodeRabbit レビュー 4 件まとめて対応。

1. playRingtone に safety timeout (30s) を追加
   deps.playRingtone の onComplete contract が将来壊れても
   scheduler が無限 hang しないよう、deps 経由で注入可能な safety
   timer を追加。発火時は警告ログを残して busy フラグを強制解放し、
   pending waiters を全部 resolve する。テスト1件追加。

2. aivis-tts.removeFile を execFile("rm") から fs.unlink へ
   Windows には rm が無くテンポラリファイルがリークしていた。
   既に node:fs/promises から writeFile を import していたので
   unlink を同時に取り込み child_process 依存を削除。

3. DEFAULT_BACKOFF_MS の 3 番目 (4000) はデッドコードだった
   MAX_RETRY_ATTEMPTS=3 なので sleep は attempt 1→2, 2→3 の 2 回
   だけ。配列を [1000, 2000] に縮め、なぜ 2 個なのかコメントで
   説明。at(-1) フォールバックはそのまま残す。

4. AivisError の cause を super のオプション引数経由に
   ES2022 の Error.cause を使って手動キャストを除去。
@MocA-Love
Copy link
Copy Markdown
Owner Author

@coderabbitai レビューありがとうございます!4 件すべて妥当な指摘でしたので e27f646f でまとめて対応しました。

  • Actionable (playRingtone 無限待機リスク): deps.ringtoneSafetyTimeoutMs (デフォルト 30s) の safety timer を追加。発火時は警告ログ + 強制解放。テスト 1 件追加。
  • Nitpick 1 (removeFile の rm): node:fs/promises.unlink に置換、child_process 依存を削除。Windows でもテンポラリファイルリークなし。
  • Nitpick 2 (DEFAULT_BACKOFF_MS の dead code): [1000, 2000] に縮小し、なぜ 2 要素で十分かコメントを追加。
  • Nitpick 3 (Error.cause): super(reason, { cause }) 構文に変更して手動キャストを削除。

再確認お願いします。

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== audio-scheduler.ts ==="
cat apps/desktop/src/main/lib/notifications/audio-scheduler.ts

echo ""
echo "=== aivis-tts.ts ==="
cat apps/desktop/src/main/lib/notifications/aivis-tts.ts

echo ""
echo "=== audio-scheduler.test.ts (safety timer test) ==="
grep -n "safetyTimeout\|ringtoneSafety\|safety\|unlink\|DEFAULT_BACKOFF\|Error.cause\|cause" apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts | head -60

echo ""
echo "=== audio-scheduler.test.ts full ==="
cat apps/desktop/src/main/lib/notifications/audio-scheduler.test.ts

Length of output: 33227


@MocA-Love: 4件の対応を確認しました。すべて正しく実装されています 🎉

指摘 確認内容
playRingtone 無限待機 ringtoneSafetyTimeoutMs?: numberAudioSchedulerDeps に追加、デフォルト 30s。onRingtoneComplete()dispose() の両方でタイマーを clearTimeout し、リーク防止も適切。テスト(20ms でタイムアウトさせ 40ms 待機)で動作を確認 ✅
removeFile の rm → unlink import { unlink, writeFile } from "node:fs/promises" に置換済み。child_process の依存が完全に除去され、クロスプラットフォーム対応と ENOENT 無視のコメントも適切 ✅
DEFAULT_BACKOFF_MS の dead code [1000, 2000] に縮小済み。MAX_RETRY_ATTEMPTS=3 では attempt 1→2 間と attempt 2→3 間の 2 回しかスリープしないためエントリ数が十分であるとのコメントも明確 ✅
Error.cause super(reason, cause !== undefined ? { cause } : undefined) に変更済み。標準の ErrorOptions 構文で手動キャスト不要 ✅

コードの品質が全体的に高く、設計も堅牢です。このままマージいただいて問題ありません。


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@MocA-Love MocA-Love merged commit fd6867d into main Apr 24, 2026
12 of 13 checks passed
@MocA-Love MocA-Love deleted the feat/desktop-audio-scheduler branch April 24, 2026 19:43
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