背景
スマホのターミナル詳細ページに会話ビューが入った(#1275 → #1751、スマホ側は receptron/mulmoserver#194)。tmux の画面キャプチャの代わりに TranscriptView を受け取り、ターンごとのブロックとして折り返して描く。
読めるのは claude のセッションだけ。codex / antigravity / grok / muse のセッションを開くと status: "none" が返り、スマホは画面キャプチャの表示に戻る。#1275 は claude を対象に閉じたので、他のエージェントをどうするかは決まっていない。
決めること
- どのエージェントを対象にするか — codex / antigravity / grok / muse のうち、会話ビューを出す価値があるのはどれか
- どの順で入れるか
- muse を含めるか — 1 つだけ sqlite を経由し、読み出しの形が他と違う(下記)
この issue は上の 3 つを決めるところまで。実装は決まったエージェントごとに別 issue へ切る(置き場も id の引き当ても畳む規則もエージェントごとに違うので、1 本にまとめると測り直しの対象が混ざる)。
判断に要るのに、この issue からは出せない材料が 1 つあります。実際にどのエージェントでセッションを回しているか。コスト側は下に揃えたので、そこに使用実績を重ねて決める形になります。
基準として想定しているのは「そのエージェントでスマホから会話を追いたい場面がどれだけあるか」で、実装コストはその次です。会話ビューが無いエージェントも壊れるわけではなく、これまでどおり画面キャプチャの表示に戻るだけなので、対象から外す判断も普通に取れます。
判断材料
対象範囲
getTerminalTranscript(sessionId) が答える範囲、つまりいま開けるセッション。launcher の "or resume here" が並べる過去の会話を読めるようにする話は含まない。
1 本目だけが払うコスト
入口は server/index.ts の captureTerminalTranscript: (sessionId) => sessionTranscriptView(cwdOfSession(sessionId), sessionId) で、server/session/transcript-view-read.ts の sessionTranscriptView が claude の置き場を直接組み立てている — cwd から ~/.claude/projects/<encoded-cwd>/ を導き、MulmoTerminal のセッション id をそのままファイル名として <id>.jsonl を開く。他のエージェントはこの場所にファイルを持たないので none になる。
エージェントで分岐していないのは意図的で、ここが実装の最大の制約になる。同関数の doc コメントいわく、agentOfSession(server/index.ts)は再起動をまたいだ claude セッションを shell と報告する — claude pane の pane_current_command がバージョン文字列(2.1.233)で、agentFromPaneCommand に該当エントリが無いため。だから今は agent を聞かずファイルの存在を聞いている。素直に agentOfSession で reader を選ぶ実装にすると、再起動後の claude セッションが会話ビューを失う。
つまり「reader をどう選ぶか」の骨格を最初の 1 本が作る。2 本目以降はその骨格に乗る。ただし乗るのは骨格だけで、下の「レコード → 行」はエージェントごとに測り直しになるので、2 本目が無料になるわけではない。
エージェント別のコスト
| agent |
置き場と、セッション id からの引き当て |
レコード → 行の下地 |
| grok |
~/.grok/sessions/<encodeURIComponent(cwd)>/<uuid>/。引き当て不要 — このサーバが --session-id <UUID> で自分の id を渡して会話を作るので、セッション id が grok の会話 id(server/agents/grok-session.ts、grok 0.2.118 で実測済み) |
無し。ただし updates.jsonl を transcript-fold で追う前例はある(#1377) |
| codex |
~/.codex/sessions/<日付ツリー>/rollout-*.jsonl。rollout id は codex が自分で mint するので別物で、対応は server/session/agent-conversations.ts の append log が持つ。id からファイルへは codexRolloutPath(server/agents/codex-sessions.ts) |
あり — server/agents/codex-events.ts が event_msg の payload を読み、server/session/last-turn.ts の lastTurnFromCodexRollout がターン境界と最終返信を既に取り出している |
| antigravity |
brain ディレクトリ。同じ append log を引く。cwd は agy 自身のファイルからは答えられないので、これが唯一の経路(server/agents/antigravity-sessions.ts 冒頭) |
無し。加えて prompt が <USER_REQUEST> / <ADDITIONAL_METADATA> / <USER_SETTINGS_CHANGE> でラップされるので、剥がす処理が要る |
| muse |
~/.local/share/muse/session-index.db(sqlite)を session_id で引き、その行の session_log_path が指す session.jsonl を読む(workspace_root は cwd の照合用。server/agents/muse-session.ts) |
無し。ただし server/agents/muse-usage.ts が同じログを transcript-fold で追っていて、sqlite を経由する読み出し自体はバッジのポーリングで既に動いている。ログは実測で 1 日 33 MB に育つ |
JSONL の head を読む部分(server/agents/transcript-head.ts)は codex / antigravity / grok の 3 つで既に共有されている。ただしこれは launcher の一覧がタイトルを取り出すための共有で、会話ビューの畳み処理はここに含まれない。
気をつけること
- 畳む規則が claude のレコード形に対して書かれている。
server/session/transcript-view.ts のターン境界(user プロンプトのレコード)、TOOL_RESULT_MAX_LINES、thinking を落とす判断、知らない content ブロックを unknown 行にする扱いは、いずれも claude の JSONL を実測して決めたもの。エージェントごとに測り直すことになる
TRANSCRIPT_LINE_BUDGET / TRANSCRIPT_MAX_BYTES はエージェントに依らない上限なので、そのまま効く
status: "cleared" は claude 固有。/clear が新しい id と新しい transcript を作る前提(server/session/cleared-transcripts.ts)。他のエージェントに同等の状態があるかは未調査で、無ければ単に返さなければよい
- スマホ側は、ホストが同じ
TranscriptView の形で status: "ok" を返す限り変更が要らない。描画は kind(user / assistant / tool / unknown)だけを見ていて、どのエージェントの会話かは見ていない。エージェント名を表示に出したくなった場合だけ、スマホ側にも作業が出る
考えられるアプローチ
スコープの候補を 3 つ挙げます。どれも「1 本目が reader の選び方の骨格を作る」ぶんを含みます。
- A: grok だけ — 骨格を最も安く確かめられます。id の引き当てが無く claude と同じ形(セッション id で直接引ける)なので、新しく書くのは畳む規則だけ
- B: grok + codex — A に、畳む規則の下地がある codex を足す形。会話ビューが claude / codex / grok で使えるようになります
- C: 全部 — antigravity と muse まで。この 2 つは畳む規則を丸ごと新規に書くことになり、antigravity は prompt のラップを剥がす処理、muse は sqlite を経由する読み出しが加わります
関連
本文の実装記述は remote main(a71872a1)のソースを読んで書いています。
背景
スマホのターミナル詳細ページに会話ビューが入った(#1275 → #1751、スマホ側は receptron/mulmoserver#194)。tmux の画面キャプチャの代わりに
TranscriptViewを受け取り、ターンごとのブロックとして折り返して描く。読めるのは claude のセッションだけ。codex / antigravity / grok / muse のセッションを開くと
status: "none"が返り、スマホは画面キャプチャの表示に戻る。#1275 は claude を対象に閉じたので、他のエージェントをどうするかは決まっていない。決めること
この issue は上の 3 つを決めるところまで。実装は決まったエージェントごとに別 issue へ切る(置き場も id の引き当ても畳む規則もエージェントごとに違うので、1 本にまとめると測り直しの対象が混ざる)。
判断に要るのに、この issue からは出せない材料が 1 つあります。実際にどのエージェントでセッションを回しているか。コスト側は下に揃えたので、そこに使用実績を重ねて決める形になります。
基準として想定しているのは「そのエージェントでスマホから会話を追いたい場面がどれだけあるか」で、実装コストはその次です。会話ビューが無いエージェントも壊れるわけではなく、これまでどおり画面キャプチャの表示に戻るだけなので、対象から外す判断も普通に取れます。
判断材料
対象範囲
getTerminalTranscript(sessionId)が答える範囲、つまりいま開けるセッション。launcher の "or resume here" が並べる過去の会話を読めるようにする話は含まない。1 本目だけが払うコスト
入口は
server/index.tsのcaptureTerminalTranscript: (sessionId) => sessionTranscriptView(cwdOfSession(sessionId), sessionId)で、server/session/transcript-view-read.tsのsessionTranscriptViewが claude の置き場を直接組み立てている — cwd から~/.claude/projects/<encoded-cwd>/を導き、MulmoTerminal のセッション id をそのままファイル名として<id>.jsonlを開く。他のエージェントはこの場所にファイルを持たないのでnoneになる。エージェントで分岐していないのは意図的で、ここが実装の最大の制約になる。同関数の doc コメントいわく、
agentOfSession(server/index.ts)は再起動をまたいだ claude セッションをshellと報告する — claude pane のpane_current_commandがバージョン文字列(2.1.233)で、agentFromPaneCommandに該当エントリが無いため。だから今は agent を聞かずファイルの存在を聞いている。素直にagentOfSessionで reader を選ぶ実装にすると、再起動後の claude セッションが会話ビューを失う。つまり「reader をどう選ぶか」の骨格を最初の 1 本が作る。2 本目以降はその骨格に乗る。ただし乗るのは骨格だけで、下の「レコード → 行」はエージェントごとに測り直しになるので、2 本目が無料になるわけではない。
エージェント別のコスト
~/.grok/sessions/<encodeURIComponent(cwd)>/<uuid>/。引き当て不要 — このサーバが--session-id <UUID>で自分の id を渡して会話を作るので、セッション id が grok の会話 id(server/agents/grok-session.ts、grok 0.2.118 で実測済み)updates.jsonlをtranscript-foldで追う前例はある(#1377)~/.codex/sessions/<日付ツリー>/rollout-*.jsonl。rollout id は codex が自分で mint するので別物で、対応はserver/session/agent-conversations.tsの append log が持つ。id からファイルへはcodexRolloutPath(server/agents/codex-sessions.ts)server/agents/codex-events.tsがevent_msgの payload を読み、server/session/last-turn.tsのlastTurnFromCodexRolloutがターン境界と最終返信を既に取り出しているserver/agents/antigravity-sessions.ts冒頭)<USER_REQUEST>/<ADDITIONAL_METADATA>/<USER_SETTINGS_CHANGE>でラップされるので、剥がす処理が要る~/.local/share/muse/session-index.db(sqlite)をsession_idで引き、その行のsession_log_pathが指すsession.jsonlを読む(workspace_rootは cwd の照合用。server/agents/muse-session.ts)server/agents/muse-usage.tsが同じログをtranscript-foldで追っていて、sqlite を経由する読み出し自体はバッジのポーリングで既に動いている。ログは実測で 1 日 33 MB に育つJSONL の head を読む部分(
server/agents/transcript-head.ts)は codex / antigravity / grok の 3 つで既に共有されている。ただしこれは launcher の一覧がタイトルを取り出すための共有で、会話ビューの畳み処理はここに含まれない。気をつけること
server/session/transcript-view.tsのターン境界(user プロンプトのレコード)、TOOL_RESULT_MAX_LINES、thinkingを落とす判断、知らない content ブロックをunknown行にする扱いは、いずれも claude の JSONL を実測して決めたもの。エージェントごとに測り直すことになるTRANSCRIPT_LINE_BUDGET/TRANSCRIPT_MAX_BYTESはエージェントに依らない上限なので、そのまま効くstatus: "cleared"は claude 固有。/clearが新しい id と新しい transcript を作る前提(server/session/cleared-transcripts.ts)。他のエージェントに同等の状態があるかは未調査で、無ければ単に返さなければよいTranscriptViewの形でstatus: "ok"を返す限り変更が要らない。描画はkind(user/assistant/tool/unknown)だけを見ていて、どのエージェントの会話かは見ていない。エージェント名を表示に出したくなった場合だけ、スマホ側にも作業が出る考えられるアプローチ
スコープの候補を 3 つ挙げます。どれも「1 本目が reader の選び方の骨格を作る」ぶんを含みます。
関連
getTerminalTranscriptを答えるようにした回本文の実装記述は remote main(
a71872a1)のソースを読んで書いています。