ブログ
Mac コンパニオンアプリの内部構造:軽量セッションマネージャーのアーキテクチャ
Mac コンパニオンアプリは Tactic Remote を静かに支える基盤です。メニューバーに常駐するユーティリティで、tmux セッションの管理、組み込み HTTP/WebSocket サーバーの実行、AI 作業中の Mac のスリープ防止を担います。
このカテゴリのプロダクト/開発アップデートを確認できます。
カテゴリを見るMac コンパニオンアプリは Tactic Remote で最も目立たない部分でありながら、おそらく最も重要な部分です。メニューバーに常駐し、マシン上のすべての Claude Code セッションを管理し、iPhone が接続する API を提供し、スリープサイクルやネットワーク変更にまたがってセッションを維持するという地味ながら重要な作業を担当しています。この記事では、その仕組みと、各アーキテクチャ上の判断に至った理由を説明します。
設計上の制約
開発の初期段階で3つの制約を設定しました:
- アイドル時にアプリの存在を感じさせないこと。 開発者はすでに数十のバックグラウンドプロセスを実行しています。アイドル状態で目立つ CPU やメモリを消費するセッションマネージャーは許容できません。
- 複数の並行セッションを処理できること。 多くの開発者は Claude Code タスクを2つか3つ同時に実行します。機能ブランチ用、テスト用、ドキュメント用といった具合です。
- Mac のスリープやネットワーク変更に優雅に対応すること。 開発者がノートPCを閉じてカフェに移動し、再び開いたとき、すべてが数秒以内に再接続されるべきです。
これらの制約がアーキテクチャのあらゆる層を形作りました。
組み込みサーバーモデル
コンパニオンアプリは別プロセスとしてサーバーを実行するのではなく、HTTP と WebSocket サーバーを直接組み込んでいます。これは、スタンドアロンのデーモンを同梱する方式に対する意図的な選択でした。
なぜ組み込みなのか? 独立したデーモンには、独自のライフサイクル管理が必要です。launchd の設定、プロセス監視、ログローテーション、クラッシュリカバリーなど、それぞれが潜在的な障害点であり、サポート負担になります。サーバーをアプリ自体に組み込むことで、ライフサイクルはシンプルになります。アプリが動けばサーバーも動く。アプリを終了すれば、すべてがクリーンに停止します。
組み込みサーバーは設定可能なポート(デフォルト 48736)にバインドし、localhost のみ(ローカルネットワークモード、別途ネットワークプロキシ層を使用)またはすべてのインターフェースで待ち受けます。セッション管理とステータスクエリ用の REST API と、リアルタイムのターミナルストリーミングおよび承認プッシュ用の WebSocket エンドポイントを公開しています。
アイドル時、サーバーはソケットを開いたままにしますが作業は行いません。ベンチマークでは、アクティブなセッションがない状態のコンパニオンアプリのメモリ消費は約 18 MB、CPU 使用率はほぼゼロで、一般的なメニューバーユーティリティと同等です。
セッション基盤としての tmux
Tactic Remote が管理するすべての Claude Code セッションは tmux セッション内で実行されます。screen や直接的な PTY 管理ではなく tmux を選んだ理由はいくつかあります:
セッションの永続性。 tmux セッションは、それを作成したプロセスとは独立して存続します。コンパニオンアプリがクラッシュして再起動しても、tmux セッションとその Claude Code プロセスは中断されることなく動作し続けます。アプリは起動時に既存のセッションに再アタッチするだけです。
干渉なしのキャプチャ。 tmux の capture-pane コマンドにより、入力を注入したり実行中のプロセスを変更したりすることなくターミナルの内容を読み取ることができます。これは極めて重要です。Claude Code の動作を妨げるリスクなしに、ターミナル出力を iPhone にストリーミングする必要があるからです。
名前付きセッション。 各 Claude Code セッションには予測可能な tmux セッション名(例:claude-remote-0、claude-remote-1)が付けられ、プログラムからのセッションの列挙、アタッチ、管理が容易になります。
成熟して広く普及。 tmux は macOS に同梱されているか、Homebrew で簡単にインストールできます。数十年の安定性の実績があり、我々が自力で解決しなければならないエッジケース(ターミナルのリサイズ、Unicode、エスケープシーケンス)を処理してくれます。
コンパニオンアプリは設定可能な間隔(デフォルト 200ms)でアクティブな tmux セッションをポーリングし、最新のターミナル内容をキャプチャします。内容に変更があると、差分を WebSocket 経由で接続中のクライアントにプッシュします。このポーリング方式は意図的にシンプルにしたものです。ファイルシステムベースの通知メカニズムを評価しましたが、macOS のバージョン間で、この間隔での直接的なポーリングほど信頼性がないことが分かりました。
セッションライフサイクル管理
Tactic Remote における Claude Code セッションは定義されたライフサイクルを経ます:
- 作成 — iPhone が「セッション開始」リクエストを送信。コンパニオンアプリは新しい tmux セッションを作成し、指定された作業ディレクトリとタスク説明で Claude Code を起動します。
- アクティブ監視 — ターミナル内容がキャプチャされストリーミングされます。Claude Code の hook システムからの承認リクエストがインターセプトされ、WebSocket とプッシュ通知を介して iPhone に転送されます。
- アイドル検出 — セッションのターミナル出力が設定可能な期間変化しない場合、ポーリング頻度を下げてリソース使用量を削減します。
- 完了 — Claude Code が終了。コンパニオンアプリが終了を検出し、最終出力をキャプチャして iPhone に通知します。
- クリーンアップ — tmux セッションは設定可能な保持期間(デフォルト 30 分)保持されるため、ユーザーは出力を確認でき、その後自動的にクリーンアップされます。
複数の並行セッションの管理は、これらのライフサイクルを独立して多重化することを意味します。各セッションは独自のキャプチャループ、独自の WebSocket チャンネル、独自のステートマシンを持ちます。コンパニオンアプリは現在最大 8 つの並行セッションをサポートしており、この制限はアーキテクチャ上の制約ではなく実際のリソーステストに基づいています。
Mac のスリープ防止
これはあまり明白ではない技術的課題の一つでした。macOS は非アクティブを検出すると積極的にスリープに入りますが、tmux 内で実行されている Claude Code タスクは、macOS が「アクティビティ」として認識するようなユーザー入力を生成しません。
コンパニオンアプリは、少なくとも1つの Claude Code セッションがアクティブに実行されている間、IOPMAssertionCreateWithName API を使用して電力アサーションを保持します。アサーションタイプは kIOPMAssertPreventUserIdleSystemSleep で、ディスプレイの消灯は許可しつつシステムのスリープを防止します。すべてのセッションが完了または終了すると、アサーションは即座に解放されます。
外部プロセスとして caffeinate を使用することも検討しましたが、独立したデーモンを却下したのと同じ理由で却下しました。ライフサイクルの複雑さが増すからです。IOKit アサーション API は正確な制御を提供し、アプリ自体のセッション追跡とクリーンに統合できます。
メニューバー統合
コンパニオンアプリはメニューバーアイコンと最小限のドロップダウンインターフェースで表示されます。メニューには以下が表示されます:
- 接続ステータス — サーバーが動作中か、どのポートにバインドされているか、Cloudflare Tunnel がアクティブかどうか。
- アクティブセッション — 実行中の Claude Code セッション数と簡単なステータス表示。
- QR コードアクセス — iPhone ペアリング用の接続 URL と認証トークンを含む QR コードをワンクリックで表示。
- クイックアクション — サーバーの開始/停止、設定を開く、接続 URL のコピー。
フルウィンドウインターフェースの構築は意図的に避けました。コンパニオンアプリの役割はインフラであり、操作の目的地ではありません。開発者は iPhone を通じて Tactic Remote と対話します。Mac アプリが注意を必要とするのは、初期設定時と時折の設定変更時のみであるべきです。
Cloudflare Tunnel 統合
Cloudflare Tunnel モードが有効な場合、コンパニオンアプリは cloudflared プロセスを子プロセスとして管理します。設定済みのトンネル認証情報で cloudflared を起動し、ローカルメトリクスエンドポイント経由でヘルスを監視し、トンネルが切断された場合は自動的に再起動します。
この統合は微妙なタイミングの問題に対処しています。iPhone がトンネル経由で接続できるようになる前に、トンネルが完全に確立されている必要があります。コンパニオンアプリは cloudflared が正常な接続を報告する(通常、起動後 1〜3 秒)のを待ってから、トンネル URL を利用可能として通知します。この間、メニューバーアイコンは独自の「接続中」状態を表示します。
トンネルプロセスがクラッシュした場合、コンパニオンアプリは指数バックオフ(2秒、4秒、8秒)で最大3回の再起動を試み、それでも失敗した場合はトンネルモードを失敗としてマークし、接続中のクライアントにローカルモードへのフォールバックを通知します(利用可能な場合)。
認証
すべての API リクエストと WebSocket 接続には Bearer Token が必要です。トークンは初回起動時に生成され、macOS キーチェーンに保存されます。セットアップ時に表示される QR コードに含まれるため、ペアリングは1回のスキャンで完了し、手動でトークンをコピーする必要はありません。
トークンは SecRandomCopyBytes を使用して生成された 256 ビットのランダム値です。セッションベースの認証ではなく静的トークンモデルを選択したのは、主な脅威モデルが不正なネットワークアクセスであり、マルチユーザーアクセス制御ではないためです。ユーザーごとの認証が必要なチームには、トンネルの前段に Cloudflare Access を配置することを推奨します。
検討したが採用しなかった案
フル Electron アプリ。 メモリオーバーヘッドだけでこれを却下しました。Electron のベースラインは 80〜150 MB で、ネイティブ Swift アプリのアイドル時の約 5〜8 倍です。
スタンドアロン CLI デーモン。 初期構築はより簡単ですが、技術に詳しくないユーザーにとっては管理が難しく、macOS メニューバーの慣例との統合も不可能です。
tmux の代わりに直接 PTY 管理。 より細かい制御が可能ですが、tmux がすでに確実に処理しているセッションの永続性、ターミナルエミュレーションのエッジケース、プロセスライフサイクル管理を再実装する必要がありました。
自動起動用の LaunchAgent。 これを実装しましたが、デフォルトではなくオプトインにしました。開発者はログイン時に何を実行するかについて強い意見を持っており、我々はそれを尊重します。
パフォーマンスプロファイル
通常の負荷(2つのアクティブセッション、1台の接続済み iPhone)でのコンパニオンアプリのリソース使用量:
| 指標 | 値 |
|---|---|
| メモリ (RSS) | 22-35 MB |
| CPU(アイドル、出力変更なし) | < 0.1% |
| CPU(アクティブストリーミング、2セッション) | 1-3% |
| ディスク I/O | ほぼゼロ(デフォルトではディスクにログ記録なし) |
| ネットワーク(ローカルモード、アクティブストリーミング) | 10-50 KB/s |
これらの数値は macOS 14 を実行する MacBook Pro M2 で測定されたものです。Intel Mac でのパフォーマンスも同等ですが、アクティブストリーミング時の CPU 使用率はやや高くなります。
今後の展望
コンパニオンアプリのアーキテクチャは、現在出荷済みの機能を超える能力をサポートするよう設計されています。セッション管理層は、セッション共有(複数の iPhone が同じセッションを監視)、セッション録画(再生用の完全なターミナル履歴のキャプチャ)、そして Claude Code の拡張性 API の進化に伴うより豊富な統合に対応できます。
セットアップ手順についてははじめにを参照してください。Mac コンパニオンアプリのトラブルシューティングについてはよくある問題を参照してください。
Tactic Remote を試す
スマートフォンからコーディングエージェントを操作
Mac、Windows、Linux 上の Claude Code や Codex などに接続。iPhone や iPad で進捗を確認し、次の指示を送れます。