Spotlight に 653 個の agent セッションを食わせた
私のマシンには Codex CLI と Claude Code のセッションが 653 個転がっている。Codex が 440、Claude が 213。~/.codex/sessions と ~/.claude/projects という二つの隠しディレクトリに、日付とエスケープされたパスで階層化されて散らばり、ファイル名は UUID の羅列だ。三日前のあるセッションに戻りたければ、まずそれがどのプロジェクトのものだったかを思い出し、ディレクトリを一層ずつ掘り、見つけたら見つけたで正しい作業ディレクトリで resume しないと立ち上がらない。たいていは掘るのが面倒で、新しいセッションを開いてコンテキストをもう一度食わせ直していた。
同じマシンで、⌘ + Space は毎日何十回も押している。アプリを開くのも、為替を調べるのも、ファイルを探すのも全部これだ。それなのに、こいつは私のセッションを知らない。
Spotion はこの両端をつなぐツールだ。2400 行の Swift で、自分のウィンドウを持たない。インターフェースは Spotlight そのもので、メニューバーのアイコンはディレクトリを見張ってインデックスを新鮮に保つ役だけを負う。いまは ⌘ + Space を押して soldy auth と打ち、リターンを押せば、ターミナルが正しいディレクトリでそのセッションを復元する。新しいセッションもここから始める。New Codex Session を選び、インラインで prompt を打ってリターンで開始だ。
四つのアイデアはどう却下されたか
手を動かす前に Gemini と一巡り話した。きっかけは Notomo というプロジェクトを見かけて、その路線がとても気に入ったことだ。ユーザーのデータを移行させず、ユーザーの習慣も変えず、システムの支援機能だけでネイティブアプリの上に薄い層を被せる。ユーザーがいつか嫌になったら、切ればネイティブ体験に戻る。この種のツールは開発が速く、試す敷居が低く、離脱コストはゼロだ。
その路線でアイデアを出させた。四つ出てきて、一つずつ却下した。
- Apple Mail の AI サイドバー。本当に agent にメールを処理させたいなら、mail CLI を繋ぐ方が直接的だ。
- カレンダーとリマインダーのクイック入力。コマンドラインで元々できる。
- Safari の読書体験強化。私は Safari を使わない。
- QuickLook に AI 質問応答。方向は少し面白いが、場面が広すぎる。保留。
四つ目を却下して、ようやく全部が同じ場所で負けていると見えた。四つとも、すでにより短い経路があることにインターフェースを足していたのだ。一日中ターミナルに浸かっている人間にとって、インターフェースが一つ増えることはコストが一筆増えることで、どれだけ綺麗に作っても同じだ。
私のセッション問題は逆向きだった。データはディスクに転がっていて、欠けているのは入口だ。そして入口なら手元にもうある。Spotlight は何年も筋肉の記憶に住んでいる。やるべきことは一つだけ、データをそこに食わせることだった。
要件を書き出すと三行しかない。過去のセッションを検索でき、リターンで直行する。新しいセッションを Spotlight から発起できる。インデックスは自動で新鮮に保たれる。この三行は後に、ほぼそのまま README の機能リストになった。
一巡り調べて、既存のものはなかった
Raycast や Alfred のプラグイン生態にはセッション管理があるが、⌘ + Space をそれらに置き換えるのが前提だ。OpenAI 自家製の Codex デスクトップ版には完全なセッションリストがあるが、あのデータはアプリの中に閉じ込められていて、システムからは検索できない。コミュニティのメニューバーツールは自前の検索ボックスを作る——つまり覚えるべきショートカットがもう一つ増える。どこも自分のインターフェースを作っていて、システムに既にあるあの入口へデータを渡す者はいなかった。
一方で Core Spotlight も App Intents も公開 API で、最初から最後まで hack は要らない。空隙はそこに置いてあり、技術的な障害はなく、ただ誰も両端をつないでいなかっただけだ。
名前もあの会話から転がり出た。Spot に Session を足すと、読みが Potion にぶつかる。だから Spotion。
公式推奨の API、呼び出しが出て行ったきり戻らない
週末プロジェクトのつもりだった。JSONL をパースし、インデックスへ donate して、店じまい。ところが予想を超えた箇所が少なくない。まず Apple 側の話から。現行ドキュメントでは、App Entity を Spotlight へ送る標準のやり方は indexAppEntities だ。この呼び出しは私のマシン(macOS 26.5)でエラーも出さず、返っても来ない。XPC リクエストは出て行き、completion は永遠に発火せず、await はその場で固まる。サンプルはこの一台だけで、他の環境でどうかは知らない。
退路は 2015 年からある CSSearchableItem に、associateAppEntity を一行足してエンティティを関連づける方法だ。Spotlight が受け取る意味情報は一つも欠けず、しかも呼び出しはちゃんと返ってくる。
固まりに怯えた後、すべてのプロセス間呼び出しに二十秒のタイムアウトを巻いた。
try await withTimeout(20, operation: "index \(batch.items.count) items") { done in
self.index.indexSearchableItems(batch.items) { done($0) }
}
タイムアウトは「実行されなかった」ではない
タイムアウトは、私がもう待たないというだけの意味だ。向こうの呼び出しはまだ走っている。
最も典型的なケースはこうなる。deleteAll がタイムアウトし、失敗として扱い、こちらが持つインデックス済み id の台帳を残し、通常のリフレッシュに降格する。数十秒後、あの呼び出しのコールバックが突然戻ってきて、成功を報告する。インデックスは本当に空にされていた。台帳はまだ数百件が健在だと主張しているから、以後の増分リフレッシュは毎回「補うものはない」と判断する。ユーザーに落ちる現象は、Spotlight のセッションが前触れなく全部消え、再起動しても戻らない、だ。
この種のタイミング問題は頭の中だけでは網羅できない。基盤の PR には Codex の review bot が 28 件の finding を残し、十巡り直して、テストは 30 本から 45 本に増えた。五、六巡り目で分かった。これらの finding は同じ一つのことを言っている。私はローカルの辞書を扱う直感のまま、別プロセスの中の状態を操作していた。あのインデックスは現状を照会できず、書き込みにトランザクションがなく、タイムアウトの後から成功することもある。三つ揃えば、書き方を丸ごと変えるしかない。
具体的に何を変えたか、三つ挙げる。
まず donate の確認とファイルキャッシュを切り離した。以前は mtime が変わらなければ一切をスキップしていて、upsert が一度タイムアウトすると、インデックス内の古いメタデータは永遠に誰にも触られなくなる。いまは独立した dirty 集合があり、donate の成功が確認されて初めて帳消しにする。キャッシュヒットはもう「スキップしてよい」を意味しない。
掃除の順序も逆転させた。以前は先に deleteAll、次に台帳を消し、それから再投入。削除が一度タイムアウトすると台帳はすでに消えていて、インデックスに実在する古い項目はもう誰にも認識されない。いまは削除の成功が確認されてから台帳に手を付け、失敗なら台帳を残し、報告し、降格する。再構築の義務は UserDefaults にも落としてあり、プロセスが殺されても次回起動が続きから再試行する。
タイムアウトした呼び出しにはフックを一つ残してある。コールバックが後から戻ってきたら、フックが成功か失敗かを見分け、成功なら補償を発火して、ゾンビがこっそり消したものを一括で再投入する。より綺麗に見えて採らなかった案もある。パイプラインを塞いでゾンビの終了を待つことだ。この呼び出しの元々の故障モードは「永遠に終わらない」であり、それを待つのは、偶発の事故を恒久のデッドロックへ昇格させることになる。
一台のマシン、2400 行のコード、ネットワークには触れない。それなのに後半はずっと分散システムの練習問題だった。
読んでいる間も、ファイルは伸びている
予想を超えた二つ目はデータソースだ。Codex の rollout ファイルは 13MB まで育つことがあり、読んでいる間も追記されている。この種のファイルは丸ごとは読めない。スキャナは先頭と末尾から有限のウィンドウだけを読み、ウィンドウ境界の欠けた半行はそのまま捨てる。
タイトルには優先順位がある。カスタムが最上位、次が AI の付けた題、その下が最後の prompt、最後の受け皿が最初の実 prompt だ。そしてこの受け皿は、巨大な注入コンテンツの塊に最初の読み取りウィンドウから押し出されることがある。私の初版はウィンドウ拡張の条件を「meta が見つからなければ拡張し続ける」と書いたが、meta はよりによって一行目にある。かくして一部のセッションは永遠にプロジェクト名をタイトルとして被り続けた。このバグは二度直した。Codex 側で一度、Claude 側でそっくりそのまま一度。review では独立した二件の finding だった。
リリース前に実データで全量を一度回した。1121 ファイルが重複排除で 653 セッションになり、パース失敗はゼロ。
見つからないのは、私の問題かシステムの問題か
もう一つ、気に入っている小さな設計がある。セッションが Spotlight に出てこないとき、こちらの donation が届いていないのかもしれないし、システム側が表示していないのかもしれない。設定のトグルが切られているか、インデックスサービスの機嫌が悪いか。二種類の故障は、ユーザーから見える現象がまったく同じだ。
だから設定ページに自己診断ボタンを置いた。CSUserQuery で Spotlight の UI を迂回し、インデックス本体を直接照会する。見つかれば、こちら側の仕事は済んでいる——システム設定のトグルを確認しに行く。見つからなければ、その場で再構築する。README のトラブルシュート第一条がこれだ。まず自己診断、それから手分けして対処。
自分を書いたセッションまで索引した
Spotion は Codex と Claude Code で書かれた。リポジトリのブランチは半分が claude/xxx、半分が codex/xxx で、review も Codex の bot がやった。書いている過程で生まれたそれらのセッションは、インストールした途端すべて検索できるようになった。
この先のイテレーションも同じ線をたどる。リターンはターミナルを開くほかに、セッションを Claude や Codex のデスクトップアプリへ渡せるようにもなる。アイコン、Sparkle の自動更新、Homebrew での配布、このあたりは定番の作業だ。今日に至るまで、自分のウィンドウは持っていない。
プロジェクト全体を終えて、覚えておきたいことは一つだけだ。あるコードがローカルコードかどうかは、どのマシンで動いているかで判断しない。タイムアウトしうる、トランザクションがない、状態を照会し返せない——この条件が揃った呼び出しはリモートリクエストであり、リモートリクエストの流儀で仕えなければならない。
ソースは github.com/Iris-Ares/Spotion、macOS 26 が必要。