Skip to content

3 accounts

Claude Lin & Lay edited this page Oct 18, 2026 · 1 revision

起動とアカウント

Codex CLI の起動と再開(#272)

種別で Codex CLI を選び、CLI コマンド(codex または実行ファイルの絶対パス)と作業ディレクトリを設定する。対応基準は Codex CLI 0.160.0 以上であり、古い CLI では起動前に診断を返す。アプリはインストール・認証・利用者のグローバル設定を変更しない。宣言済み cli は command が codex でも汎用のままであり、移行対象は旧 ai または kind の無いアカウントだけである。

Codex の部屋の MCP は起動の行だけで渡し、ファイルには書かない(#297)。 起動の -c mcp_servers.<自席>={…} が定義全体(コマンド・引数・env_vars・enabled=true・部屋の六道具の承認)を運ぶ。席の名前は Claude の席と同じアカウント/トピックごとの名前(server_name_for)である。席の属性(部屋 URL・名前・アカウント・部屋 id・色・未読の印)と部屋トークンは起動した CLI の環境にあり、定義は名前だけを env_vars で指す。部屋トークンと起動別のセッション操作 capability(#415)は設定・argv・プレビューに値を残さない。発言・履歴・利用量と、セッション操作の三道具を起動時に承認する。行に載るのは自席の定義だけで、他の席の名前は載らない——Codex の他の席の部屋サーバーはそれぞれの起動の行にしか無く、この席からは見えないため、無効化する他席の MCP 登録が無い。起動の前に有効 cwd に .codex フォルダーだけを用意する。Codex 0.160.0 の native discovery は config.toml の無い .codex フォルダーでも project layer を返し(loader の discover_project_layers)、専用 hook の置き場はその layer から決まる(下記)。

以前の版が .codex/config.toml に書いた部屋の登録は、その席が起動するときに消す(#297)。 消すのは名前が pullcept-room で始まり、env に PULLCEPT_ACCOUNT_ID を持つ欄だけで、部屋 URL を問わない。他のキー・欄・コメントは残し、消す欄が無ければファイルを書き直さない。欄を消して [mcp_servers] が空になればその表も外す。ファイルそのものは消さない。対象は起動する有効 cwd のファイルだけであり、別のフォルダーに残った欄は、そのフォルダーで席が起動したときに消える。残っていても無効(enabled = false)で、サーバーは起動しない。消してよいのは、#272 以来どの版も同じ定義全体を起動の行で渡しており、ファイルの欄は既定で無効のまま一度もサーバーを起動していないためである。

Claude の席にそろえる形と比べて、書かない形を選んだ(#297)。 Claude の席の .mcp.json は一つの登録をアカウント/トピックごとに書き、今の部屋 URL を持つ欄を残して古い URL の欄を消す(#40)。Claude Code は MCP をそのファイルから読むので、ファイルの欄が要る。#297 より前の Codex の登録もこの粒度と残し方に既にそろっていたが、ファイルの欄は起動の -c が丸ごと置き換え、使われていなかった。残る違いは、欄がファイルに溜まり、他の席を止める -c …enabled=false がその欄を数えて増えることだけだった。書かない形なら、欄が溜まらず、他の席を止める行も要らず、未信頼のフォルダーでも同じに動く。Claude の席の .mcp.json への書き込みと読み出しは変えない。

-C パス / --cd パス / --cd=パス は元の process cwd に対して解決し(短い -C=パス は明示拒否する)、設定登録・hook discovery・画面の cwd に同じ有効 cwd を使う。実対話の profile・-c・環境変数を保持する。app-server は --profile を受け付けないため、discovery には選んだ $CODEX_HOME/<名前>.config.toml の project_root_markers だけを渡し、利用者の -c をその後に適用する。profile 名は native runtime が受理する plain name を使う。0.160.0 はドットを含む名前を拒否する。読めない profile や安全に保持できない markers は拒否する。remote 接続・CLI 自身による --worktree 作成・非対話サブコマンドは、このローカル席の起動欄では使えない。

初回は診断端末でフォルダーと専用 hook を信頼する。 SessionStart の一覧で codex-session.mjs と、ファイル方式なら Li+ の codex-output-style.py を確認し、それぞれの handler を信頼する。アプリは既存 handler と信頼状態を保持し、信頼 hash を書かず、全 hook の確認を迂回しない。MCP instructions は札・返信・履歴を案内する。キャラクターは下記の方式で選ぶ。AGENTS.md と Li+ の指示は CLI が既存どおり読み込む。パネルの五欄は rollout から読む(下記「Codex の席は自分の様子を rollout で知らせる」)。

hook の場所は同条件の native config/read の project layer から決め、登録後に hooks/list でも列挙を確認する(未信頼フォルダーは通常の CLI 信頼画面へ進む)。standalone cwd・独立 Git root・サブディレクトリに加え、linked worktree では正式な root-checkout mapping を使う。.git の gitdir、commondir、登録 checkout、main checkout の所属を照合し、同一 Git repository の対応する相対位置に専用 handler だけを追記する。実際に書く設定/hook の境界外 symlink は拒否する。native loader 0.160.0 と trust resolver に合わせた動作である。

初回の実会話で hook stdin の session_id を受け、起動 nonce・アカウント・トピック・生きている PTY を照合して索引へ保存する。最初のターン前は ID 未取得が正常である。子セッション・古い起動・別の席・保存 ID と異なる通知は採用しない。停止後は保存 UUID を codex resume <id> に渡す。CODEX_HOME の会話ファイルが正常な探索で見つからない時だけ新規に戻り、権限/I/O エラーで調べられない時は ID を保持して明示 resume を試す。過去を押し込まず、必要な履歴は席が道具で引く。

Codex の席は代替画面を使わずに起動する(#293)。 新規起動の行と再開の行(codex resume <id> …)の両方の末尾に --no-alt-screen を足す。Codex 0.160.0 の TUI は既定(tui.alternate_screen = "auto")で代替画面に描くため、端末ペインの scrollback に履歴が溜まらず、上へスクロールできない。このフラグで TUI は inline で描き、ペインに履歴が残る(codex --help / codex resume --help の「Runs the TUI in inline mode, preserving terminal scrollback history.」)。clap は再開の行でも位置引数の後のフラグを受け取る。入力欄は最下行にあり、キーを打つと最下部へ戻る(xterm.js の既定動作)。起動オプションが画面の扱いを既に決めていれば足さない——--no-alt-screen そのもの、または -c / --config の値で鍵が tui.alternate_screen のもの(-c key=value と --config=key=value のどちらの書き方でも)である。二重に指定して起動を落とさないためであり、利用者の指定がそのまま効く。プレビューは起動と同じ合成を通るため、この語もプレビューの行に出る。Claude Code の行には足さない。

Codex のキャラ選択(#281)

キャラクター欄には .codex/output-styles/character_codex_luna.md の 拡張子を除いた名前 character_codex_luna を入力する。大文字小文字を区別し、ASCII の英数字で始まる英数字・_・- の128文字以下とする。空欄は project .codex/config.toml の [liplus] output_style に任せる。selector 未設定でも .codex/output-styles があれば character_instance.md を既定に使う。false は空欄時に persona を無効にする。アカウントに名前を指定すれば既定の本文を選択本文で置き換え、一つだけ配送する。共通 developer_instructions は変更しない。Claude と同じ artifact を共有できるが、Claude 側の共有リンク・frontmatter の設定は環境移行時に行う。

native config/read が返した有効 project layer のうち、selector 又は style フォルダーを持つ最優先の root を採用する。disabledReason のある layer を参照 root にせず、profile の project_root_markers、手動 -c、--cd、CODEX_HOME の discovery 条件を保持する。style 選択自体は project 設定専用で、profile/global/-c に置いた [liplus] との連動は約束しない。

Python 3.11 以上と、その root に installed Li+ helper .codex/hooks/codex-output-style.py が必要である。loader 契約 の resolve --cwd <有効cwd> --root <信頼済みroot> [--style <アカウント名>] をプレビューと起動・再開で共通に使う。本文を返させず、protocol_version 1、mode、name、root、sha256、byte_count、handler metadata を検証する。本文 parser は Li+ helper だけが持つ。

専用 handler は native hooks/list の project source、enabled、trusted、同期実行、additionalContextLimit=0、startup/resume/clear/compact の一回ずつの登録を確認する。登録が無い・変更されて再信頼が必要・別 root の style handler と重複する場合は起動を止める。プレビューに方式と選択名を表示する。missing、空本文、不正 UTF-8、frontmatter 不整合、境界外 symlink/junction、上限128 KiB超などは loader と同じ結果で拒否し、旧方式には戻らない。エラーへ本文・CLI stderr・秘密を出さない。

アカウントの非空選択名だけを app-owned LI_PLUS_OUTPUT_STYLE に設定し、子プロセスへ渡す。本文は argv と環境へ載らず、長い本文でも Windows の8191文字制約を消費しない。空欄では inherited override を除去して project の既定を使う。原本 config と style、共通指示は更新しない。codex-session.mjs は ID callback 専用のままである。実環境での全文配送と画面起動は、Li+ loader 配備・個別 hook trust・新 Pullcept build の配備後の検証を要する。

移行前の inline 選択(#276)

selector も .codex/output-styles も無い project では、次の従来方式を維持する。

キャラクター欄には character_Codex_Lin のように見出しの名前を正確に入力する。定義元は、実 CLI がその起動条件で解決した 有効な developer_instructions 一文字列だけである。ファイルを別途検索したり、複数 layer の本文を連結したりしない。CLI の通常の優先順位(user base → 選択 profile-v2 → 信頼済み project layer → 手動 -c、managed 設定は CLI の制約に従う)で一つの本文が決まる。上位の文字列は下位の文字列を置換するため、共通部分は有効な本文の中へ置く。未信頼の project 本文は採用しない。

モデルを呼ばない native debug prompt-input を、元 cwd・--cd・--profile・-c・CODEX_HOME・アカウント環境で実行する。ランダムな developer_instructions sentinel を上書きした比較実行と role/content 全体を照合し、その一か所の本文だけを取り出す。本文が無い時は別の permissions/skills/AGENTS を拾わない。比較が曖昧、TOML が壊れている、CLI が失敗、出力が8 MiB超、又は各実行が15秒超なら起動を止める。CLI stderr や他の指示・秘密はエラーへ出さない。

定義の例:

developer_instructions = """
共通指示をここへ置く。
# character_Codex_Lin
NAME=Codex Lin
NICKNAME=Lin
Lin の本文。
## 詳細
ここも Lin の本文。
# character_Codex_Lay
NAME=Codex Lay
NICKNAME=Lay
Lay の本文。
# 共通の続き
この見出し以降は両方に残る。
"""

列0の # character_<名前> が開始、次の列0の H1 (# ) が終端である。開始見出しの末尾空白を除いた文字列が選択名となる。名前の空白と空の名前は拒否する。H2以下、空行、区切り線は本文の一部で、バッククォート又はチルダ3個以上のコードフェンス内の見出しは区切りにしない。フェンスは同種で開始以上の長さ、残りが空白の行で閉じる。LF/CRLF・空白・区切りを保持し、選択したブロックを元位置に残して、他のキャラブロックだけを除く。キャラ外の共通指示はそのまま残る。全定義を検査し、重複・空本文を拒否する。未知名は別キャラへ代替しない。

選択した本文を最後の -c developer_instructions=... として合成する。TOML Unicode escape で日本語・改行・引用符・シェル文字を保持する。手動 -c の文字列/配列/inline table も同値に符号化し、持ち運べない他のオプションは省略せず拒否する。短いフラグの値は空白で分ける(-p 名前、-C パス、-c key=value)。-p名前/-p=名前 等の連結・等号形式は探索と起動で明示拒否し、長い --profile=名前/--cd=パス/--config=key=value は扱う。Windows の合成行が8191文字を超える時も切り詰めず拒否する。Codex の PTY は cmd /C call で空白を含む .cmd コマンドを保ち、文字列の % は TOML escape で渡す。プレビューと起動・再開は同じ helper/parser/argv 合成を通る。プレビューは編集したコマンド・環境も使い、失敗理由を表示する。

inline 方式の空欄は developer_instructions 上書きを行わず既存 CLI に任せる。旧保存値は削除しないが、自由文から名前選択へ意味が変わるため、そのままでは未知名として拒否する。Codex の PULLCEPT_CHARACTER を MCP instructions へ送る旧経路は無い。原本 developer_instructions・AGENTS.md・Li+・グローバル設定は変更しない。既存 app-owned MCP の登録更新と ID 専用 SessionStart hook の追記は従来どおりである。Claude の output style 選択は従来どおり。

以後の .mcp.json・--settings・output style・--session-id は Claude Code の作法であり、Codex の設定と初回 ID 取得はこの節を参照する。

Codex の席を app-server 経由で起動する(#299)

Codex の標準起動は、席ごとの app-server 方式である。 新規アカウントと codex_app_server が欠落した設定では有効になり、明示保存された false(hook 方式)と true は保持する。他の種別から Codex CLI に変更するときも標準は有効である。アカウントの窓で種別が Codex CLI のときだけ「app-server 方式」の欄が出る。外して決定すれば次の起動から互換の hook 方式(上の #281)に戻る。編集・保存・プレビュー・実際の起動で同じ選択を使い、走っている席は切り替えても変わらない。2026-10-17 の実機往復と既定切り替えの依頼を受けて変更した(「実装状況」の #299 の検証範囲)。スマホ接続は別の opt-in で既定オフのままであり、Claude など他の種別の起動は変えない。

一つの席に一つの app-server を立て、その上に本物の TUI を繋ぐ。 起動は次の順に進む。

  1. キャラクターを #281 と同じ loader で解決する。file mode では helper の hook を、hook がするのと同じ入力(SessionStart、startup、有効 cwd)と同じ環境(LI_PLUS_OUTPUT_STYLE に選択名)で一度だけ走らせ、hook が配るはずの本文を受け取る。本文の UTF-8 の長さと sha256 が resolve の metadata と一致しなければ、配らずに起動を止める(sidecar/src/codex-style.mjs の body)。移行前の inline 選択(#276)では、有効な developer instructions から見出しを選んだ本文を使う。persona が無効(false)なら本文は無い。
  2. 席の app-server を codex <オプション> -c mcp_servers.<自席>={…} -c features.codex_hooks=true -c hooks.state={…} app-server --listen ws://127.0.0.1:0 --ws-auth capability-token --ws-token-sha256 <digest> で起動する。部屋の MCP の定義は #297 と同じものを、今度は server の行に載せる。部屋の変数(部屋トークン・URL・名前・アカウント・部屋 id・色・未読の印)は server の環境にある。待ち受けの港は OS に選ばせ、server が標準出力に書く listening on: の行から読む。
  3. アプリが server に繋ぎ(Authorization: Bearer)、initialize、hooks/list、thread/start(再開なら thread/resume)を送る。新しい会話には thread/inject_items で印を一つ記録する(下記)。
  4. 端末で codex resume <会話 id> --remote ws://127.0.0.1:<港> --remote-auth-token-env PULLCEPT_CODEX_REMOTE_TOKEN を起動する。TUI は自分で thread を始めず、アプリが用意した会話に繋がる。

キャラクターは thread/start の developerInstructions で渡し、hook では渡さない。 二重に配らないために、その席の server でだけ Li+ の output style の SessionStart handler(.codex/hooks/codex-output-style.py を hook で呼ぶもの)を止める。止めるのは -c hooks.state={'<hook の鍵>'={enabled=false}} であり、鍵は native hooks/list が返す <hooks.json の絶対パス>:session_start:<組>:<番号> を #281 の検証で特定した一つだけである。-c の値は既存の hooks.state に再帰的にまざるので、その hook の信頼(trusted_hash)は残る。Li+ の他の hook(cold-start、Trigger Check Gate など)は止めない。-c hooks.state."<鍵>".enabled=false の書き方は使えない:Codex は -c の鍵を . で区切るため、パスを含む鍵が割れる(codex-rs/config/src/overrides.rs)。thread を作る前に hooks/list を読み直し、止めたはずの鍵が enabled: false で並び、codex-output-style.py を呼ぶ有効な SessionStart hook が一つも残っていないことを確かめる。残っていれば起動を止める。ファイル(hooks.json・config.toml)は書き換えない。

再開でも毎回 developerInstructions を送り直す。 thread/resume にも同じ本文を載せる。アカウントのキャラクターを変えた後の再開は、新しい本文で始まる。

file mode の developerInstructions は、利用者の共通指示、検証済みキャラ本文、部屋の一文の順に合成する(#303)。 共通指示は #276 と同じモデル不要の native discovery(codex-character.mjs)で、その席のコマンド・cwd・起動オプション・環境から解決した有効な developer_instructions 一文字列である。空または未設定なら追加せず、取得失敗は起動・再開を止める。各本文の間に空行を置き、キャラ本文の改行・UTF-8 のバイトは変えない。本文の長さと sha256 の照合(上の 1.)はキャラ本文だけに行い、共通指示と部屋の一文は照合後に合成する。部屋の一文は Claude の席が --append-system-prompt で受け取るものと同じ関数(crates/mcp-config の room_system_prompt)で作る(下記「席は部屋の道具の名を起動時に受け取る」)。起動と毎回の再開は同じ合成を thread/start と thread/resume に載せる。inline 選択では選択済み本文が共通指示を既に含むので、再追加せず、その後に部屋の一文だけを足す。

キャラの本文が無い席は、CLI 自身の有効な developer instructions を先頭に置く。 developerInstructions は CLI の developer_instructions を置き換える(codex-rs/core/src/config/mod.rs、developer_instructions.or(cfg.developer_instructions))。persona が無効(false)の席や、inline 方式で名前を選んでいない席では、部屋の一文だけを送るとその人の指示が消える。そこでその席では #276 と同じモデル不要の読み取り(codex-character.mjs)で有効な本文を読み、その後に部屋の一文を足す。有効な本文が無ければ部屋の一文だけを送る。読み取りに失敗すれば起動を止める。

会話 id は thread/start の答えから取り、codex-session.mjs は使わない。 hook 方式では初回の実会話で hook が id を知らせていた(#272)。app-server 方式では起動した時点で id が決まるので、新しい会話はその場でトピックに記録する。この席では codex-session.mjs を hooks.json に登録せず、server の環境に PULLCEPT_NATIVE_URL も PULLCEPT_LAUNCH_ID も置かない。以前の起動が残した登録があっても、その補助は環境が無ければ何も送らずに終わる。五欄(#283)と利用上限の扱い(#294)はこの id から rollout を読むので、最初の会話を待たずに働き始める。

空の会話には thread/inject_items で印を一つ記録する。turn/start は使わない。 Codex は記録の無い会話を rollout に書かず、記録の無い会話に TUI は繋がれない。そこで新しい会話に developer の発言を一つ、Pullcept opened this conversation as a seat in a room. Room posts arrive as user input. と記録する。比べた二つの方法は次のとおりで、前者を選んだ。

  • thread/inject_items:モデルを呼ばない。キャラクターの口に言葉を入れない(記録は developer 役であり、キャラの発言ではない)。TUI はすぐ繋がり、席は最初の発言を待つ。
  • 最初の部屋の発言で turn/start を打つ:その発言が来るまで端末を立てられず、席が開かない。一往復ぶんのモデル呼び出しがかかる。発言は端末を通らずに入るので、利用上限で止まった席の預かり(#294)や、人が打ちかけのときの保留を迂回する。

認証トークンは最初から必須であり、認証の無い server は立てない。 トークンは起動のたびに新しく作る(UUID v4 二つ、乱数 244 ビット)。server が受け取るのは sha256 の digest だけで(--ws-token-sha256)、トークンそのものは argv・設定・プレビューに載らない。端末には環境変数 PULLCEPT_CODEX_REMOTE_TOKEN で渡し、行にはその名前だけが載る。アカウントの環境変数でこの名前は上書きできない(APP_LAUNCH_ENV)。待ち受けは 127.0.0.1 だけである。トークンの無い接続・違うトークンの接続は HTTP 401 で拒まれる(2026-10-06、0.160.1 の隔離 CODEX_HOME で実測)。

起動オプションは、server・端末・会話のどれかに行き先がある物だけを受ける。 行き先が無い物は省かずに起動を止める(プレビューと起動の前に同じ判定)。

  • 会話へ(thread/start と thread/resume の引数):-m/--model、-a/--ask-for-approval(untrusted/on-request/never)、-s/--sandbox(read-only/workspace-write/danger-full-access)、--yolo(--dangerously-bypass-approvals-and-sandbox、承認 never と sandbox danger-full-access になる)、-c model=…、-c approval_policy=…、-c sandbox_mode=…、-c model_reasoning_effort=…。旗は同じ項目の -c より強い。-c の物は server の行にも載る。
  • server の行へ:それ以外の -c(tui.* を除く)、--enable、--disable。
  • 端末の行へ:-c tui.*、--no-alt-screen(#295)。人が画面の方式を選んでいなければ --no-alt-screen を足す。
  • -C/--cd:作業ディレクトリとして読み、行には載せない。
  • それ以外(-p/--profile、--search、-i、--oss など)は拒否する。

server の答えに入る model・承認・sandbox・effort を、渡した値と照らす。違えば起動を止める。

server は席が閉じるときに必ず止める。 三つの止め方を重ねる。

  • 端末の終わり:席ごとの見張りが 500 ms ごとに端末を見て、終わっていれば server を木ごと止める(taskkill /T /F)。
  • アプリの終わり:全端末を閉じるとき(kill_all_ptys)とアプリのイベントループの終わり(RunEvent::Exit)に、全部の server を止める。
  • アプリのプロセスの終わり(強制終了を含む):server は作られた直後に KILL_ON_JOB_CLOSE の job object に入り、アプリのプロセスが消えると OS が木ごと止める(Windows)。

起動の途中で失敗したら、作った会話を thread/delete で消してから server を止める。端末の起動に失敗した場合も同じである。

プレビューは二行を出す。 アカウントの窓のプレビューは、方式・server の行・端末の行を出す。トークンの digest、港、会話 id は起動時に決まるので、<sha256>、<port>、<会話 id> の印で示す。

起動オプション

セッションの起動オプションはアカウントの属性であり、アカウントのモーダルで編集する(下記「アカウント」)。決定するまで保存しない。

アプリは channel のエントリを起動の行へ足さない(#195)。 部屋の入力路は端末への入力であり、サイドカーは channel を宣言しない。以前はアプリ自身の server:pullcept-room-<slug>-<hash> を利用者の --dangerously-load-development-channels へ統合していた。利用者が自分の channel サーバ(たとえば github-webhook-mcp)を書いていれば、それは書かれたまま載る——その人の行であり、部屋は触らない。--channels も同じ理由で拒否しなくなった。

Claude Code の起動する行へアプリが足すのは --settings の JSON 文字列である。部屋のサーバの承認(enabledMcpjsonServers)と、起動しない兄弟の登録(disabledMcpjsonServers)と、アカウントのキャラクター(下記「アカウントはキャラクターを持つ」)がそこに載る。承認するサーバの名前はアカウントごとに異なり、.mcp.json の登録鍵と同じ 1 つの事実である(下記「.mcp.json の登録単位」)。書いた行と走る行は違う。

実際に起動する行は画面に表示する。書いた行と走る行が違う以上、結果を見せるほうが説明より安い。表示は起動オプションの欄の隣、つまりモーダルの中に置く。編集している欄の隣であることが条件であり、欄が移れば表示も移る。名前の欄を編集しても表示は動かない。承認する名前がアカウント id から決まるためであり、これは要点である——起動しているのはアカウントであって、改名はそれを別のものにしない。

表示は省略記号で切らずに折り返す。切れる末尾はアプリが足した --settings であり、そこが「書いた行と走る行が違う」ことの中身である。一行のランチャ行には縦の余裕が無かったが、モーダルにはある(ランチャ行そのものは #62 で無くなった。下記「ランチャ行は無い」)。

起動オプションは、その人が足したいものだけを書く欄である(#156、決定3)。CLI ごとの作法は種別が持つ(下記「種別は CLI の作法を持つ」)。セッション id の渡し方はその筆頭であり、種別が Claude Code の席には --session-id {session_id} が種別の側から載る——ここへ書く欄ではなくなった。

種別 CLI(汎用) にはその作法が無い。{session_id} をここへ書けば、新しいセッションを始めるときに、このアプリが決めた UUID がそこへ入る(「トピック」)。書かなければ id は配られない。

この欄が起動の行へ載らないことがある(#154。下記「人が書いた値は、起動の行が運べる形でだけ載る」)。cmd.exe が反応する文字を持つ引数が一つでもあれば、欄ごと載せずに起動する。起動は止めない。

id の差し込みは、行を組み立て終えてから一度だけ行う。 種別が載せた置換子と、人が書いた置換子は同じ一つの置換子である。片方を先に埋めれば、もう片方は置換子のまま CLI へ渡る。同じ理由で、既に id の置き場を持っている行——再開の行、あるいは自分でフラグを書いた行——へ種別の側から二本目を足さない。一つのフラグが二つ並んだときに CLI がどちらを読むかを、このアプリは確かめていない(--settings の併記を拒否するのと同じ地面である)。

実測済み(2026-08-21): 複数の channel サーバを同時に有効化した状態で往復が成立した(部屋の入力路が channel だった間の記録。#195 で外れた)。--print での確認は無意味である(対話の入力欄が走らないため)ことも実測しており、確認は対話セッションでのみ可能。

再開コマンド

再開コマンドは {session_id} を含む一行であり、先頭の語が command になる(例: claude --resume {session_id})。トピックがアカウントごとのセッション id を持ち、再開時にそこへ差し込む(「トピック」)。置換は crates/mcp-config/ に置き、テストの対象とする。

戻り方は、まず種別の答えである(#156、決定6)。CLI を名指す種別はその CLI の戻り方を作法として持つため、欄を出さない——既に答えのある問いに二つ目の答えを置けば、二つは食い違いうる。欄を持つのは CLI(汎用) であり、アプリが答えを持たない起動はそこで人が書く。

一行まるごとである。 起動オプションと別の欄なのは、戻り方が起動と同じ呼び出しとは限らないためであり、これは人がそのとき打つ行である。分割は起動オプションと同じ規則で行う。

CLI を名指す種別では、再開の行にもアカウントの起動オプションが載る(#167)。載る位置は戻り方の語(--resume {session_id})の後ろである。再開は同じ席の同じ CLI であり、その人がすべての起動に求めたこと——--dangerously-skip-permissions がその筆頭である——はこの起動にも求められている。載らなければ、セッションは出ていったときと別のモードで戻る(2026-09-26、Master 観測。--resume で起動した二本の claude.exe のコマンドラインに、起動オプションのどのフラグも無かった)。#156 以降この種別の再開の行は画面に出ないため、人が書き足して補う道も無い。

  • 載せ方は新規起動と同じ規則である。 載せられない値を持つ起動オプションは欄ごと落ち(下記「人が書いた値は、起動の行が運べる形でだけ載る」)、戻り方の語は残る。判定は戻り方の語と合わせる前に起動オプションだけに対して行う——合わせてから判定すれば、落ちる欄が戻り方の語を道連れにし、再開のつもりで新しいセッションが立つ。
  • 種別の作法が id を渡すためのフラグ(--session-id)は載せない。 戻り方の語が既に id を名指しており、一つの行に id の置き場を二つ作らない(上記「起動オプション」の一度だけの差し込みと同じ地面である)。
  • CLI(汎用) では足さない。 その再開の行は人が欄に書いた一行まるごとであり、起動オプションを求めるならそこへ書ける。その CLI の戻り方が起動のオプションを受け取るかどうかを、このアプリは知らない(#156、決定5)。

空は正当な状態であり、通常の状態である。 そのアカウントは開き直したトピックへ新規のセッションとして着席し、必要になったことは read_room_history で自分から引く。resume を持たない CLI も同じ状態に置かれる——種別がその CLI の戻り方を持たない場合も同じである。

この一行が起動の行へ載らなければ、その起動は拒否する(#154。下記「人が書いた値は、起動の行が運べる形でだけ載る」)。起動オプションのように落として起動する形は採らない——引数を落とした再開コマンドは再開ではなく、戻ったつもりで新しいセッションが立つ。

作業ディレクトリ

セッションの作業ディレクトリはアカウントの属性であり、アカウントのモーダルで表示・変更する。未設定のままアプリのプロセス cwd へ暗黙にフォールバックしない。未設定のアカウントの起動は拒否し、モーダルを開く。

npm run tauri dev ではアプリのプロセス cwd が src-tauri/ になるため、暗黙のフォールバックはセッションを src-tauri/ で起動させ、.mcp.json もそこへ書く。作業ディレクトリはユーザーの選択であって、アプリのプロセスがたまたま居た場所ではない。作業ディレクトリを持たないアカウントにはホームディレクトリを prefill するが、これは既定値ではなく prefill である。画面に出ていないディレクトリでセッションを起動しない。

アカウント

アカウントは、起動していなくても存在する同一性である。作る、残る、起動する、止める。名簿は「今つながっている人」ではなく「誰が居るか」を示す面になる。

同一性は id にある。 アカウントは一意な id を構造的に持ち、それだけが同一性である。名前・色・作業ディレクトリ・起動オプションはすべて属性であり、編集できる。名前が識別子ではなくなったため、改名しても、名前が衝突しても、誰が誰かを見失うものが無い。

アカウントは #40 の機構を置き換え、目的を保つ。 #40 が直した欠陥は「タブが起動の手段でしかなく、同一性を持てなかった」ことである。同一性が無いところに名前を付ける唯一の形が「起動ごとに宣言する」だった。構造的な同一性ができたので、名前と色はそこへ移る——そして起動時宣言では持ちようがなかったものを得る。動いていないアカウントにも名前と色がある。 #40 が守ろうとした「名指せること」は、より強い形で満たされている。

アカウントは種別を持つ。 admin(人間)、Claude Code、Codex CLI、CLI(汎用)、mcp(ローカル MCP サーバ)の五つである(#156 決定1、mcp は #193)。人間の種別は #192 まで user と呼んでいたものであり、名前を役割に合わせて admin に改めた(Master 判断、2026-09-28)。振る舞いは変えていない。種別は作成時の宣言であり、部屋も画面もそれを推測しない——部屋は接続の種類しか知らず、別のクライアントから参加した人間はセッションと同じ種類の接続で来るため、推測すれば取り違える。宣言できる唯一の時点はアカウントを作る瞬間であり、そこには既にフォームがある。

起動する種別は三つあり、違いはアプリがその下のコマンドについて何を知っているかである。 Claude Code と Codex CLI はこのアプリが作法を持つ CLI を名指す(下記「種別は CLI の作法を持つ」)。CLI(汎用) はそうでない起動であり、行はその人のものになる——部屋の席が要るもののほかに、アプリは何も足さない。Codex CLI は #272 で追加された種別であり、保存名は codex_cli である。

種別が決めるのは、参加者のリストのグループ(「参加者パネル」)と、起動する行が持つ作法の二つである。部屋の参加者は依然として一種類であり、フレームにも名簿の判定にも種別は現れない。

既存の config.json の種別は起動コマンドから読み直す(下記「既存設定からの移行」)。

画面の本人は種別 admin のアカウントである。 #53 が保留にした判断をここで決めた(#59)。本人が行を持てなかったことは、参加者のリストが二つに割れていた一因でもある。移行は localStorage の名前と色をそのアカウントの属性にするだけであり、どちらのキーもこれ以降は書かない。どのアカウントが本人かは localStorage(pullcept.local-account)が持つ——本人であることはこの画面の性質であってアカウントの属性ではなく、同じ config を別の画面で開けば本人は別になる。

本人のアカウントは削除できず、種別も admin から変えられない。部屋に居ることが「ここに居る」ことそのものであり、削除すれば居るための何かが無くなる。種別を変えれば、本人が起動する種別の見出しの下に並び、その名前で CLI を起動する選択肢が出る。

種別 admin のアカウントは起動しない。人間の下に spawn する CLI は無く、start_session が拒否する。その行に開始も出さない——拒否が唯一の知る手段になるのは、拒否の使い方として誤りである。稼働中のアカウントの種別は変更できない。そのセッションが起動した作業ディレクトリと起動オプションを、動いている最中に捨てることになるためである。

名前は人間が読んで選ぶものである。 UUID などの機械的な一意化で置き換えない。宛先の候補に出るのはニックネーム(未設定時はこの名前)であり、そこから選べることが目的である。id が機械的な一意性を担うのは、名前をその責務から解放するためである。新規アカウントの既定名は既存と重ならない番号を取る——既定値が同じ名前の参加者を並べるのでは、#40 の欠陥を既定で再現することになる。

名前が空のままの決定は拒否する。

アカウントの一覧と稼働中の名簿は、アカウント id で突き合わせる。 名前で結ばない。名前は衝突しうるものになり、稼働中に改名されることもある。突き合わせの結果は一つのリストである(「参加者パネル」)。

オフラインのアカウントはリストに出し、宛先の候補には出さない。 宛先の候補の出所は部屋の名簿だけであり(「参加者パネル」)、席の無いアカウントはそこに存在しない。届かない相手を名指せる状態は、宛先を名簿から引くという判断(#43)の理由に反する。

種別は CLI の作法を持つ

CLI ごとの作法はアプリが 1 か所で持つ(#156、決定2)。持ち手は種別であり、置き場は crates/mcp-config/ の Cli である。答えは五つある。

作法 Claude Code
セッション id の渡し方 起動する行へ --session-id {session_id} を足す
再開の仕方 claude --resume {session_id}
--settings のステータスライン 載せる(#155)
会話の在り処 <home>/.claude/projects/<作業ディレクトリの slug>/<id>.jsonl(#131、決定3)
部屋の道具の名乗り 起動する行へ --append-system-prompt で載せる(#147)

欄でなくなった理由は実測である(2026-09-17、Master 観測)。Claude Lin のセッション id が「なし」だったのは、そのアカウントの起動オプションに --session-id {session_id} の宣言が無かったためである。id の受け渡しはこのアプリの機能であり、人が手で書く形は、書き忘れた瞬間に静かに効かなくなる。同じ構造がステータスライン(#155)にもあった——Claude Code の設定の書き方でありながら、当時は起動する全アカウントに載っていた。撤去済みの hook(#149)も当時は同じ位置に在った。

答えは種別ごとの分岐である。 二つ目の CLI は五つの答えそれぞれの二つ目の腕であり、最初の CLI を前提にした場所をアプリ中から探し直すことではない。腕を書き忘れればコンパイルが名指す。

CLI(汎用) はこの表を持たない(決定5)。起動する行はその人のものであり、アプリは自分の知っている形をその上へ載せない——載せられる正しい形を持たないためである。知らないものを推測で埋めない。{session_id} の手書きの宣言はこの種別では有効なままである(上記「起動オプション」)。

その種別が失うのはステータスラインと名乗りである。 パネルの五欄は「—」のままになり、その行の note に「制限中」も出ず(出所が五欄のうち二つであるため。#161)、その席は部屋の道具の名を起動時に受け取らない。部屋の登録の承認(#143)と、他のアカウントの登録を止めること(#103)と、channel エントリの統合(上記「起動オプション」)は種別に依らず載る——それは部屋の席が要るものであって、CLI について知っていることではない。

会話の在り処も種別の答えである。 再開の前に会話が実在するかを見る判断(「トピック」、#131)は、その CLI の置き場を知っていて初めて成り立つ。知らない種別では「分からない」であり、「無い」ではない——記録は残る側へ倒す。

席は部屋の道具の名を起動時に受け取る

起動する行へ一文を載せる(#147、決定1)。--append-system-prompt であり、内容は、その席が部屋へ発言するときに呼ぶ道具の完全名(mcp__<.mcp.json の登録鍵>__say_to_room。上記「.mcp.json の登録単位」)と、端末への出力が部屋に届かないことの二つ、それに部屋の詳しい作法の在り処である(#301)。

作法の本文は写さず、在り処だけを指す(#301)。 作法はサイドカーの instructions(「部屋の作法」)の一か所にだけ在る。一文は「部屋の詳しい作法は部屋の MCP サーバ mcp__<登録鍵> の instructions に在り、届いたらそれに従う」と言う。instructions は MCP サーバが繋がった後に届くため、最初の部屋の発言より遅れることがある。最初の発言から効かなければならないのは道具の完全名の方であり、それは「道具の一覧が届く前でもこの名前で呼ぶ」として残す。

Codex の席にも同じ一文を渡す(#301)。 app-server 方式の席は developerInstructions のキャラの本文の後に載せる(上記「Codex の席を app-server 経由で起動する」)。本文は Claude の席と同じ関数から作り、種別ごとに書き分けない。プレビューには「部屋のルール(developerInstructions のキャラクターの後)」として本文を出す。Claude の席では起動の行の --append-system-prompt にそのまま見える。

hook 方式の Codex の席には渡さない(#301)。 調べた結果は次のとおりである。

  • Li+ のキャラの配送とは衝突しない。Li+ の output style hook は本文を SessionStart の additionalContext で配り(sidecar/src/codex-style.mjs の body が読む hookSpecificOutput.additionalContext)、developer_instructions とは別の入口である。
  • だが -c developer_instructions=… は CLI の全 layer(user・profile・信頼済み project・手動 -c)で決まった有効な本文を置き換える。部屋の一文を足すには、起動のたびに有効な本文を読み、それに一文を足した全文を -c で載せ直すしかない。
  • それは #301 当時の既定の起動経路(hook 方式の file mode)に、今は走らないモデル不要の debug prompt-input 二回(各15秒上限)と、その失敗による起動停止を新たに持ち込む。さらに #281 の file mode は本文を argv にも環境にも載せない契約であり(要件)、有効な本文の全文(日本語は TOML の Unicode escape で数倍に膨らむ)を起動の行に載せればその契約を破り、Windows の 8191 文字の上限で起動を拒否する席を新たに生む。

したがって hook 方式の席は従来どおりサイドカーの instructions だけで作法を受け取る。部屋の一文が要る Codex の席は app-server 方式に切り替える。

渡す時点が起動時であることが要件である。 道具の一覧とサイドカーの instructions は、そのセッションが最初に何か道具を呼んだ後にまとめて届く(実測、2026-09-17。返事をしなかったセッションには両方の注入が一度も無く、Bash を一度踏んだ後に注入されて以後は部屋へ返せている別のセッションと並べて確定した)。部屋の発言だけで動くセッションにはその契機が来ないため、道具の存在を知らされないまま文章だけ返す状態に入る——実機で見えていたのは、端末には合言葉を答えているのに部屋のログには何も無い形であり、記録にも say_to_room の呼び出しが一度も無かった。呼んで失敗したのではなく、呼ぶものがあることを知らない。 モデルの違いではない(同日の切り分けの一つめはそう読んだが、交絡であった)。2026-09-28 の実測では、instructions は道具を一度も呼んでいないセッションに届いていた(Claude Code 2.1.283、#195。「部屋は channel を使わなくなった」)。CLI の版で挙動が変わったと見られる。この一文は残す——古い版の CLI で同じ状態に入らないためであり、札の読み方を起動の行で補う必要はない(#195)。

サイドカーの instructions は同じことを言っているが、言うのが遅い(sidecar/src/index.ts、「部屋の作法」)。起動する行は最初のターンの手前に在る唯一の場所である。

文面は経路に依らない事実だけを持つ(決定4)。channel を前提にした言い方はしない——将来チャンネルの機構が無くなっても、「部屋へ発言する手段はこの道具である」「端末出力は部屋に届かない」は変わらないためである。#195 で部屋の入力路から channel が外れたとき、この文面は変えずに済んだ。

短く保ち、会話の作法は書かない(決定5)。道具の完全名と返信の経路だけであり、100 トークン前後に収める。作法はサイドカーの instructions と道具の説明が持っており(#273)、道具を一度でも呼んだセッションは必ず持っている(2.1.283 では呼ぶ前から持っている。上記)。二つ目の写しをここへ置けば、起動のたびに払い、かつ一つ目から離れていく。

行に載せられない文面は載せない(決定6)。判定は line_safe_text であり、line_safe_word(#155、上記「セッションは自分の様子をステータスラインで知らせる」)と同じ文字集合に空白を足したものである。違いは適用範囲である——語は --settings の JSON の中を通り、しかも空白を持たない語は Windows の引用を素通りして裸で行に乗る(portable-pty-patch/src/cmdbuilder.rs の append_quoted)。この文面はそれ自体が一つの引数であり、空白を持つ引数は同じ引用が " で包むため、中身は cmd.exe の引用の内側へ入る。だが内側か外側かを決めるのは手前の引数までの " の個数であって、この引数自身の形ではない——そこへは乗らず、文字集合の側で断る(#152 / #154)。非 ASCII も同じ理由で断るため、文面は ASCII の英語である。読み手はモデルであり、失うものが無い。載せられなかったときに失うのはこの一文であって、起動ではない。

自分で書いている行には足さない。 --append-system-prompt-file を書いている行へ足すと CLI が二つの併記を拒否して起動しない(CLI の文字列、v2.1.273)。--append-system-prompt を既に書いている行は、一つのコマンドラインに載った 2 つの同じフラグをどちらの順で読むかをこのアプリが確かめていないため足さない(--settings の併記を拒否するのと同じ地面である。上記「アカウントはキャラクターを持つ」)。

既に在る会話へは届かない。 Claude Code は system prompt を会話ごとに一度だけ記録し、以後のリクエストと再開へはその記録をそのまま送る——後から別の文面を渡しても、/compact までは変わらない(--system-prompt-snapshot の既定は on。CLI の文字列、v2.1.273)。したがってこの一文が効くのは新しいセッションであり、この変更より前に始まったトピックの再開はそのままである。この性質は CLI の文字列から読んだものであって、実機では見ていない。 2026-09-17 の切り分けでも新規起動と再開は分かれたが、そこで分かれた原因は道具の注入であり、この記録の性質ではない——同じ向きに見えることを根拠にしない。

アカウントはキャラクターを持つ

アカウントは、そのセッションが誰として喋るかを属性として持つ。値は作業ディレクトリの .claude/output-styles/ に置かれた output style の frontmatter の name: であり、ファイル名ではない(実測、2026-08-26)。

この欄は作業ディレクトリを一本化するためにある。 二つのアカウントが別々のディレクトリに置かれていた理由を実測で洗ったところ、設計上の差は output style の一枚だけだった。CLAUDE.md と settings.json の差は末尾改行、settings.local.json の差は起動のたびに増えた enabledMcpjsonServers の残骸であり、state/ は設定ではない。その一枚がアカウント側へ移れば、ディレクトリを分ける理由が無くなる。部屋の側は既にそれを想定して作られている——.mcp.json の登録鍵はアカウントごとであり、同じディレクトリへ 2 セッションが起動しても互いの識別を上書きしない(下記「.mcp.json の登録単位」)。

memory の鍵は作業ディレクトリであるから、ディレクトリを共有すれば記憶の共有はそれだけで成立する。配線は要らない。セッション履歴は同じディレクトリでも別々のファイルに分かれるため合流しない。

選択は起動引数で行い、ファイルは書き出さない。 --settings は JSON 文字列を直接受ける(claude --help の --settings <file-or-json>)。アカウントごとの設定ファイルを共有ディレクトリへ書き出す形は採らない——一本化のために共有したディレクトリを、アカウントの数だけ増やすことになる。.mcp.json への書き出しがあるのは、その登録が名前で解決される必要があるためであって(「成立条件」)、書き出しが既定の形だからではない。

--settings は作業ディレクトリの settings.json の outputStyle に勝つ(実測、2026-08-26)。したがってアプリは settings.json を書き換えない。ディレクトリの既定はそのまま残り、アカウントの選択だけがその上に重なる。キャラクターを宣言しないアカウントは outputStyle を足さずに起動し、その既定で立つ(--settings 自体は部屋の登録の承認のために載る。下記「セッションは自分の部屋の登録を承認して起動する」)。空欄は宣言しないことであり、消せば既定へ戻る。

この選択は Windows の cmd.exe /C を素通りする(実機、2026-08-26、AI が操作。「実機で確認済み」)。アプリはセッションを cmd.exe /C の下で起こすため、引用符を含む JSON 文字列がこの一段で壊れれば欄は実機で効かない。走る行と、その子の claude.exe が受け取る引数は同一であった。共有した作業ディレクトリから宣言どおりの人格が立つところまで、同日に辿っている。

キャラクターは --append-system-prompt では渡さない。 Li+ の rules/model/character.md は、キャラクターが設定されている状態を「output style のファイルが在るかつ settings で outputStyle が有効」と定義している。system prompt へ流し込む形はこの定義から外れ、実際にはキャラクターが居るのに Li+ 側は「未設定」と読む状態になる。これはこの欄についての判断であり、フラグそのものの禁止ではない——部屋の道具の名乗りは同じフラグに載る(#147、上記「席は部屋の道具の名を起動時に受け取る」)。あちらは Li+ が読む状態を一つも持たない事実であり、この定義に触れない。

欄であることが要件である。 起動オプションの文字列の中へ --settings を書いても走る行は同じになるが、それではキャラクターがアカウントの属性になっていない。名前と色が起動時宣言から属性へ移ったのと同じ軸である(上記「アカウント」、#40)。

起動オプションが自前の --settings を持ち、かつキャラクターも宣言されているアカウントは、起動を拒否する。二つは同じ設定を指しており、一つのコマンドラインに載った 2 つの --settings がどちらの順で読まれるかをこのアプリは確かめていない。片方を落とすのでも選ぶのでもなく拒否するのは、起動フラグの検査と同じ理由である(上記「起動オプション」)——黙って半分だけ効いた起動は、動いたように見える。キャラクターを宣言していないアカウントの --settings は素通りする。

載せられない名前は宣言されなかったものとして扱う(#154。下記「人が書いた値は、起動の行が運べる形でだけ載る」)。この値は --settings の JSON の文字列であり、その中身は cmd.exe から見て引用の外側に立つ(#152)。判定は line_safe_text であり、通らなければ outputStyle を載せずに起動する——空欄と同じ状態であり、作業ディレクトリの既定が立つ。失うのはキャラクターであって起動ではない。

種別 admin のアカウントはこの欄を持たない。人間は自分として喋り、起動しないため style を選ぶ瞬間が無い。

キャラクターの本文はアカウントの窓で直す(#100)

ファイルの場所は、アカウントの三つの欄で一つに決まる(2026-10-06、Master): 種別(Claude Code なら .claude、Codex CLI なら .codex)・作業ディレクトリ・キャラクターから、<作業ディレクトリ>/<.claude|.codex>/output-styles/<キャラクター>.md。「キャラクター」の項目は、名前の欄の下にこのファイルの本文の欄(複数行、中でスクロールする)とフルパスを出す。欄を持つのは Claude Code と Codex CLI だけで、CLI(汎用)・admin・mcp には出ない。Codex の席が実際に読む project root は helper が信頼済み project の層から決めるため(上記「Codex のキャラ選択」)、作業ディレクトリの親の project や --cd の先になることがある。この欄はそれを辿らず、作業ディレクトリの直下を開く。

開いたときに読み、無ければ空で開く。 種別・作業ディレクトリ・キャラクターのどれかが変われば、その場で読み直す。本文に保存していない変更があるときは読み直さず、変更が捨てられることを出して「読み直す」を置く。場所が変わったまま決定すれば、どちらのファイルへ書くのかが決まらないため止める。

決定で、本文が変わっていればそのパスへ書く。 変わっていなければ書かない。output-styles のフォルダーもファイルも無ければ作る。作るのも直すのも同じ欄一つである。取消は何も書かない。削除の操作は持たない。保存した本文が効くのは、その席の次の起動からである(欄の下に出す)。

  • 同じファイルを他のアカウントも着ているなら、その名前を出し、もう一度の決定で保存する。 保存でそちらのキャラも変わるためである。数えるのはキャラクター欄に名前を書いたアカウントだけで、空欄(作業ディレクトリの既定)のアカウントは数えない——既定の名前は Claude では settings.json の層、Codex では project の selector から決まり、この欄はそれを解かない。
  • Codex の作業ディレクトリに .codex/output-styles が無ければ、そのことを出し、もう一度の決定で作る。 フォルダーが現れると、そこで起動する Codex の席はすべてファイル方式に変わり、キャラクター欄が空の席は character_instance.md を要する(上記「Codex のキャラ選択」)。
  • 開いた後に他所でファイルが変わっていれば、上書きせずに止める。 開いた時のバイト列の指紋と照らす。
  • frontmatter の name: がキャラクター欄と違うファイルには、そのことを出す。 Claude は name: で選ぶため(上記)その名前では選ばれず、Codex の helper はファイル名と違う name: で起動を止める。

書き込みはアプリ側(crates/character-file)で行い、パスを検査する: キャラクターは区切り文字・..・Windows が予約する名前を含まない素のファイル名であること(Codex は上記の文法)、作業ディレクトリが絶対パスで在ること、書き先が junction やリンクを辿っても作業ディレクトリの中にあること。在るファイルはその場で書き換える——リンクで共有された style は一つのファイルのままである。文字コードは UTF-8 で、読めた改行の形(CRLF/LF)と BOM を保って書き戻す。UTF-8 として読めないファイルは開かない。大きさは 128 KiB まで(Codex の helper と同じ上限)。

Li+ の更新はこのファイルを上書きも削除もしない(2026-10-06、Li+ main 778d698 の原文で確認)。Li+update.md は .claude/output-styles/character_Instance.md を「無ければ作る、在れば触らない」で扱い(「If Target exists: skip (Create-only)」「No tag-based overwrite」)、他の名前には触れない。古いファイルを消す手順は .claude/rules/ と .claude/skills/ だけが対象である。Codex 側の移行 helper(scripts/migrate_codex_output_style.py)は既存の style があれば「Existing user style is protected」で止まり、他の install helper が書くのは .codex/hooks/・.codex/hooks.json・.codex/rules/ だけである。したがって名前による警告は置かない。Li+ の更新は .claude/settings.json を上書きするが、このアプリはキャラを --settings で渡すため(上記)、この欄には関わらない。

アカウントは環境変数を持つ

アカウントは、起動する CLI のプロセスへ渡す環境変数を持つ(#163)。最初の用途は、Li+ の cold-start 差分状態をセッションごとに分ける LI_PLUS_AGENT_KEY、gh のログインをアカウントごとに分ける GH_CONFIG_DIR、トークンを直接渡す GH_TOKEN の三つである。変数名はアプリが固定しない。

欄は一つであり、普通の値と秘密の値を区別しない(Master 判断)。名前=値 を一行ずつ書く。最初の = で分けるため、値は = を含んでよい。空行は読み飛ばす。名前と値の前後の空白は落とす——貼り付けたトークンの末尾の空白は、意図された値ではなく誤りである。= の無い行、名前の無い行、名前に空白を含む行、同じ名前の二度書き(大文字小文字を区別しない。Windows がそう読むため)は、行を名指して決定を止める。

値は命令文に載せず、プロセスの環境変数として渡す。 起動の命令文(launch_args)は画面にプレビューされる。--settings の env に入れる形は、命令文とプレビューの両方に値を出すため採らない。渡し方は src-tauri/src/pty.rs の spawn_pty_with_env であり、CommandBuilder::env で CLI とその子の全てへ届く。部屋のトークン(PULLCEPT_ROOM_TOKEN)が既に通っている経路と同じである。値はログにも出さない。復号できない変数の拒否は変数の名前だけを名指し、値を名指さない。

値は保存の前に Windows の DPAPI(CryptProtectData、現在のユーザーのスコープ)で暗号化する。 ログインしているユーザーに結びついた鍵で暗号化され、Claude デスクトップ(Electron safeStorage)と同じ仕組みになる。Windows の資格情報マネージャーは使わない(Master 判断)。config.json には名前が平文で、値が dpapi: に続く 16 進の暗号文で残る。アプリ固有の entropy を混ぜるため、同じユーザーが走らせる別のプログラムが素の CryptUnprotectData で開くことはできない(秘密ではなく、条件を一つ足すものである)。

Windows 以外では暗号化せず、保存もしない。 DPAPI の無い環境では、値のある欄の決定を理由を示して止め、保存済みの値は開けない。平文で保存する代替は採らない——値を平文で置かないという約束が、その経路でだけ黙って外れることになる。このアプリは Windows 向けである。

画面は値を持たない。 欄に描くのはアプリの側が作った 名前=<マスク> の行だけであり(config::account_env_text)、暗号文を開けるのはアプリの側だけである。マスクは先頭と末尾を見せ、間を ... で置く——GH_TOKEN=github_pat...Nx4h。先頭は最大 10 文字、末尾は最大 4 文字で、どちらも値の 4 分の 1 を超えない。したがって短い値でも半分以上は隠れ、4 文字未満の値は ... だけになる。数えるのは文字であり、バイトではない。

出たままの行は、決定しても元の値を保つ。 名前がそのままで、値がその変数のマスクと同じ行は、保存済みの暗号文をそのまま使う(account_env::settle)。そのため、他の欄だけを直す編集で秘密を打ち直す必要は無く、秘密が画面に平文で戻ることも無い。それ以外の行は打たれた値であり、決定のときにアプリの側で暗号化してから保存する。消した行の変数は消える。欄に触れていなければ暗号化の往復そのものが起きない。欄の表示に失敗したときは、欄を読み取り専用のまま残し、決定しても保存済みの変数を変えない——描かれなかった空の欄を「全部消した」と読めば、変数がすべて失われる。

値の両端を囲む引用符は外して保存する(#165)。GH_TOKEN="github_pat_..." や '...' のように、同じ種類の引用符(" か ')一組で囲まれた値は、その一組だけを外した中身を値とする。引用符が片方の端にしか無い値(閉じ忘れた "github_pat_... など)や、両端の引用符の種類が違う値は、行と名前を名指して決定を止める。どちらの端が意図だったかを推測して保存すれば、誰も書いていない値が暗号化されるためである。本当に引用符で終わる値は、もう一方の種類の引用符で囲めば書ける("abc'" は abc')。引用符を外すのは、打たれた値だけである。出たままの行の判定は外す前の値で行うため、引用符を含んだまま保存された値の隠し表示も、そのままなら元の値を保つ。

保存済みの名前の値が、その隠し表示を書き換えたものに見えるときは、決定を止める(#165)。隠し表示に引用符を足したり外したりしたもの、または隠し表示の先頭と末尾を保ったまま ... の周りを打ち直したもの(長さが隠し表示より 4 文字を超えて長くないもの)がそれに当たる。暗号化すれば、本当の値が隠し表示の十数文字で上書きされ、元の値は戻らない。実機では、先頭に " が付いたまま保存されたトークンの隠し表示から " だけを消して決定し、トークンが失われた。止めるときは行と名前を名指し、隠し表示をそのまま残すか、本当の値を貼り直すよう促す。調べるのは、すでに保存されている名前(大文字小文字を区別しない)の行だけである。新しい変数の値や、... を含むだけの値(パスやオプション)は、普通の値として保存する。隠し表示が ... だけの短い値は、... そのもの(引用符付きを含む)だけを書き換えとみなす。

値はエラーにもログにも出さない。 引用符と隠し表示の書き換えによる拒否も、行の番号と変数の名前だけを名指す。

開けない値(別のユーザーや別の機械で暗号化されたもの、ファイル上で壊れたもの)は (復号できません) と描き、行は残す。そのままなら元の暗号文を保ち、消すか入れ直せばそれが効く。そのアカウントの起動は拒否する。 宣言したトークンや設定ディレクトリを欠いたまま起動すれば、CLI は別の誰かとして、何事も無いように走るためである。拒否は席を取る前に行い、何も書き出さない。

アプリ自身が起動のたびに設定する名前(PULLCEPT_ROOM_TOKEN、PULLCEPT_LAUNCHED_AS、PULLCEPT_ROOM_ID)は、欄に書けば決定を止める。 起動の時点で黙って上書きすれば、設定したように見えて効いていない変数になる。起動の組み立てでもアカウントの変数を先に、アプリの変数を後に置き、拒否が迂回されてもアプリの値が勝つ。名前の一覧は crates/mcp-config の APP_LAUNCH_ENV 一つであり、拒否も起動もそこから読む。PULLCEPT_LAUNCHED_AS がアカウントから書けると、別のアカウントの登録を自分の CLI から着席させられる(下記「セッションは自分以外の部屋の登録を起動しない」、#208)。

起動した CLI のプロセスには、どの部屋(トピック)で起動したかも載せる(PULLCEPT_ROOM_ID、crates/mcp-config の ROOM_ID_ENV、#264)。値は、その起動が書いた .mcp.json の登録の env に同じ名前で在る部屋 id と同じである。部屋のトークンはアドレスごとに一つで、同じ実行の部屋すべてが共有するため、トピックを見分けない。登録は(アカウント, 部屋)の組ごとに名前が決まり、セッションを止めても残るため、同じアカウントの登録が .mcp.json に複数並ぶことがある。プロセスの環境変数しか読めない外部のもの(フックなど)は、アカウント(PULLCEPT_LAUNCHED_AS)とこの部屋 id の二つで、自分の登録を一つに選ぶ。登録の側の形は変えない。

共有している作業ディレクトリにファイルを増やさない。 環境変数はアカウントの属性として config.json に在り、ディレクトリには何も書き出さない。キャラクターの選択を起動引数で行う理由と同じである(上記「アカウントはキャラクターを持つ」)。

人が書いた値は、起動の行が運べる形でだけ載る

Windows ではセッションの起動が cmd.exe /C を通る(src-tauri/src/pty.rs)。& | < > ^ ( ) は cmd.exe が反応する文字であり、引用の外側に立てば行はその位置で終わる。" は反応される文字ではなく数えられる文字であり、引数の引用(append_quoted)が \" へ直しても cmd.exe は数えるだけであるため、偶奇が反転して以後の中身が引用の外へ出る(下記「セッションは自分の様子をステータスラインで知らせる」、#152)。

アプリが自分で書く値はこの機構から塞いである(#152 / #155 / #147)。人が書いた値は三つ残っていた(#154、#153 のレビューで観測)——キャラクター(--settings の JSON の文字列値)、起動オプション、command である。いずれも #151 より前から在る。最も起こりやすいのはキャラクターの欄であり、自由入力がそのまま JSON の文字列値へ入るためである。

判定は二つに分かれる。分ける軸は、その値をアプリが書いたのか人が書いたのかである。

  • キャラクターは line_safe_text(上記「席は部屋の道具の名を起動時に受け取る」と同じ集合)。--settings の JSON の中の文字列値であり、その位置にアプリが置く値は既にこの集合で保っている。載せられない名前は宣言されなかったものとして扱う——空欄と同じ状態であり、作業ディレクトリの既定が立つ。
  • 起動オプションと command は console_safe。cmd.exe が反応する文字(と行そのものを終わらせる制御文字)だけを断り、残りは通す。この二つは人が打つコマンドラインであり、上の集合では成り立たない——Windows のパスは \ を持ち、フラグは = を持ち、このアプリ自身の置換子 {session_id} は波括弧を持つ。cmd.exe はどれにも反応しない。これは四つ目の綴りではない(決定5)。上の集合とは別の集合——cmd.exe が反応する文字そのもの——であり、それを書くのはここ一箇所である。#152 の判断が散文で名指していたものが述語になった。

起動は止めない。載せられない値を載せずに起動する(決定3)。#155 と #147 が採った形である。ただし command には載せないという逃げ道が無い。 載せられなければ起動そのものを拒否し、理由を画面に出す。再開コマンドは一行まるごと同じ側である——その引数は戻り方そのものであり、落として起動すれば戻らずに新しいセッションが立つ。黙って半分だけ効いた起動は、動いたように見える(起動フラグの検査、--settings の併記の拒否と同じ地面である)。

起動オプションは欄ごと落とす。一つの引数だけを落とさない(決定6)。オプションはフラグと値の組であり、値だけを落とせばフラグは行の次の引数を自分の値として取る——そこに在るのはアプリが載せた引数である。人はこの欄を一つの文字列として書くため、落ちるのもその一つである。値の中から文字を抜くことはしない——人が書いた行から、誰も書いていない行を作ることになる。落ちた欄が {session_id} を持っていた場合、その起動は id の置き場を持たない行であり、id を配らない(上記「起動オプション」の一度だけの差し込みと同じ判断である。配れば、CLI が受け取っていない id をトピックが記録する)。

モーダルは、保存の前にそれを言う(決定4)。どの欄が起動時にどうなるかを、欄を書いている最中に出す。保存は止めない——直すかどうかはその人の判断であり、( ) を含むインストール先のように直せない場合もある。拒否の側(command と再開コマンド)は起動時にも拒否が立つ。そちらが権限であり、モーダルは先に言うだけである(キャラクターと --settings の併記が既に採っている形である)。判定はアプリの側が持ち(session::launch_field_report)、画面が持つのは文である——同じ文字集合の二つ目の実装を TypeScript に置けば、テストされている一つ目から離れていく(parse_launch_options と同じ理由である)。

アカウントは画像を持てる(#236)

発言の行と通知の畳みの見出しの丸印(「会話面の見た目(#225)」)に、頭文字の代わりに出す画像である。Master「アカウントのアイコンも画像入れたりできるとなおいい」(2026-10-02)。頭文字だけでは、頭文字の同じアカウント——Claude Lay と Claude Lin はどちらも「C」——が丸印では見分けにくい。置き場所・大きさ・形式・操作の場所は、Master の「確認とらずに進めて~」(2026-10-02)を受けて AI が決めた。

操作はアカウントの窓の「画像」の欄にある。 丸印の見本と「選ぶ」「外す」を並べる。行のメニューには足さない。他の欄と同じく決定まで何も書かない(「アカウントの窓は同一 webview 内のモーダルである」)——選んでから取消せば、ファイルは残らない。「外す」は見本に画像が出ている間だけ出る。

保存されるのは、常に 128×128 の PNG 一枚である。 選んだ画像は画面の側で中央を正方形に切り抜き、128×128 に縮めて PNG にしてからアプリへ渡す。読み込める形式は png・jpeg・webp・gif であり、動く gif は一枚目になる。元の画像の大きさは問わない。渡るものが常に小さな PNG 一枚であるため、上限を設けていない。アプリの側は、渡されたものが PNG であることだけを確かめて置く。

置き場所は、アプリのデータ置き場(config.json と同じ場所)の avatars/ である。 ファイル名はアカウント id に .png を付けたもの。id のうち ASCII の英数字と - と _ 以外のバイトは %XX で書く——mcp アカウントの id は人が手で書くファイルのエントリ名から来るため(mcp-<エントリ名>)、パスの区切りを含みうる。% 自身も書き換えるため、二つの id が同じファイル名になることは無い。アプリが作る id(account-1 や UUID)は書き換わらない。

config.json が持つのは画像の有無だけである(avatar、true / false)。画像そのものを持たせると、どのアカウントを保存しても画像ごと読み書きすることになる。画像の欄を持たない既存の config.json は false として読む——欄が無かった当時のアカウントは、どれも頭文字だった。

画像を使うのはこの画面だけである。 部屋の名簿にもソケットの取り決め(crates/room-floor)にも載せない。行の丸印はその行のアカウント id で画像を引く——名前は衝突しうるため、名前では引かない。アカウントを持たない話者と、画像を持たないアカウントは頭文字のままである。画面は起動時に、画像を持つアカウントのファイルを一度ずつ読み、届いた時点で、それより前に描いた丸印も描き直す。

読めない画像は、黙って頭文字に戻す。 ファイルが無い、壊れているなど、読み込みに失敗した丸印は頭文字で描く。avatar の値は直さない——次に「選ぶ」で決定すれば、ファイルは書き直される。

アカウントを削除すると、その画像も消す。 avatar の値によらず消す——残ったファイルを見つけるものは無く、同じエントリ名で mcp アカウントを作り直せば同じ id になる。

アカウントの窓は同一 webview 内のモーダルである

作成・編集・削除はモーダルで行う。OS の別ウィンドウにはしない——一つの行為の途中の一段であり、別ウィンドウは探し、動かし、アプリの裏へ失くすものである。

モーダルは明示的な決定と取消を持つ。欄の change では保存しない。 フォームは下書きを持ち、決定して初めてアカウント一覧へ入る。+ は空のフォームを開くだけであり、決定するまでアカウントを作らない。取消は何も残さない——新規なら作られておらず、編集なら反映されていない。Escape も取消である。

これは形の移動ではない。以前は各欄が change で保存されており、+ を押した瞬間にアカウントが実在し、以後はその場で直していく形だった。決定も取消も無かった。モーダルへ移すこと自体はこれを直さない——モーダルの中で change 保存を続ければ同じである。直しているのは保存の時点である。

削除はモーダルの中にあり、2 クリックを取る。一度目は武装するだけで、二度目が実行する。稼働中のアカウントと、この画面の本人のアカウントは削除を拒否する。終了も #71 まではこの形であったが、確認のダイアログへ移った(「診断面」)。削除を揃えるかは #72 で扱う。

アカウントの窓は項目を横に並べる

窓の欄は、横のメニューで項目に分ける(#304、2026-10-06 Master 提案)。 欄が増えて一度に読めなくなったためである。スクロールだけにする案より、#100 でキャラクターの本文の欄が入った後も探しやすい。形は設定のダイアログと同じであり、左に項目の一覧、右にその項目の欄を置く。

  • 基本: 名前・種別・色・画像
  • キャラクター: キャラクター名・本文の欄(#100、下記「キャラクターの本文はアカウントの窓で直す」)
  • 起動: 作業ディレクトリ・CLI コマンド・(Codex CLI の注記と app-server 方式)・起動オプション・(CLI(汎用) の再開コマンド)・走る行の表示・起動の行に載らない値の知らせ
  • 環境変数
  • サーバ: 種別 mcp のサーバの節(「ローカル MCP サーバ」)

項目は種別が決める。 admin は起動しないため「基本」だけを持ち、起動する三つの種別は「基本」「キャラクター」「起動」「環境変数」を、mcp は「基本」と「サーバ」を持つ。項目の中で種別によって出る欄(Codex CLI の app-server 方式、CLI(汎用) の再開コマンド)は #304 の前と同じである。種別を変えて今の項目が無くなれば「基本」へ戻る。窓は開くたびに「基本」で開く。

項目は一つのフォームの面であり、別のフォームではない。 項目を切り替えても書きかけの欄は消えず、決定は見えていない項目の欄も含めて全部を読む。決定を止める理由と、削除・取消・決定は、どの項目の下にも出る。止める理由が見えていない項目の欄についてのもの(名前・種別・起動オプションの --settings・環境変数・サーバ・画像)なら、その項目へ切り替えて欄に焦点を置く——見えていない欄は焦点を受け取れないためである。

狭い窓では、横のメニューが上のタブの列になる。 欄の名前も箱の横から上へ移る。窓の幅で判定する(CSS の container query)。

「環境変数」の項目は、窓の幅に関わらず、見出しを上・欄を下に置く(#322、2026-10-07 Master 要望)。 形は Claude デスクトップの「ローカル環境を編集」に倣う。この項目の欄は一つだけであり、横に見出しを置けば、一行に一つ書く 名前=値 の幅をその分だけ削るためである。欄は項目の幅いっぱいと、残りの高さを使い切り、10 行より低くはならない。字は等幅のままであり、長い行は折り返さずに欄の中で横へスクロールする。欄の下に「1 行に 1 つ、名前=値 の形で書きます。」と書き方を添える。「キャラクター」の項目の本文の欄は 14 行の高さを既に持つため、この形にはそろえない(AI 判断)。

ランチャ行は無い

会話面の上に、アカウントの選択と参加だけを持つ行があった。#59 が名前・色・作業ディレクトリ・起動オプションをモーダルへ移した結果、残ったのは <select> と「セッション参加」の二つである。#62 で開始が参加者の行へ移り、この行に残る仕事は無くなった。

選択のためだけに残さない。どのアカウントを起動するかは、押された行が答える。 選択という行為そのものが不要になっており、選択肢を出す面を残せば、行と select という二つの「どのアカウントか」が並ぶことになる。

削除して失うものが無いことは参照から確認できる。accountEl.value を読むのは selectedAccount(開始の呼び出し元)と renderAccountOptions(その select 自身の再描画)だけであり、いずれもランチャ内部で閉じていた。パネル下部のセッションの値は選択ではなく画面に出ている端末に従う(「参加者パネル」)。

ランチャのボタンが持っていた起動中の disabled と、その失敗の表示は、行が引き継ぐ(「参加者パネル」)。ボタンは押された行のものになり、どのアカウントが起動しているかを言えるようになった——一つしかないボタンには言えなかったことである。

削除すると会話面が縦に広がる。

+ はアカウントが並ぶリストの見出しの隣に置く。作るものが現れる場所の隣である。タイトルバーが持っていた本人の名前と色の欄も外した(#59)——本人がアカウントになった以上、同じ 2 値を編集する面が二つ並ぶことになる。

一つのアカウントは一つの部屋に一席まで

同じ部屋での同じアカウントの二重起動は拒否する。アカウントは「誰か」であり、同じ誰かが一つの部屋に二つの接続で居ることを認めない。名簿に同じ同一性が二度並び、その名前への宛先が二つの着地点を持つことになる。

この規則は部屋に対して閉じている。 部屋はトピックごとに在るため(#141、「トピック」)、台帳の鍵は(トピック, アカウント)の組である。一つのアカウントが二つのトピックでそれぞれ席を持つのは意図した形であって違反ではない——離れたトピックで走らせたままのセッションと、画面が移った先のトピックで同じアカウントを起動したセッションである。部屋が一つだった間、この規則は実装上「アカウントが一度だけ動く」と見分けが付かず、その射程をここに書いておいたのは、この形を後から塞がないためだった。

判定は席の台帳(src-tauri/src/session.rs の RoomSeats)が持つ。掃除と確保は一度のロック取得の中で行う——先に確認して後から確保する形では、二つの起動が同じ空席を通り抜ける。

稼働の有無は PTY へ問う。解放を誰かが呼び忘れると、そのアカウントはアプリを再起動するまで部屋へ入れなくなる——終了したセッションが PTY の一覧から外れることを稼働の定義にすることで、忘れうる解放そのものを無くしている。

画面の再読み込みに耐える

走っているセッションは、画面の記憶が失われても操作できる。webview の再読み込みは通常の出来事であり、それでセッションが操作不能になる形のほうを直す。直す先は「再読み込みを起こさないこと」ではない(#84)。

状態は二つに割れている。席の台帳(RoomSeats)と PTY は Rust に在って再読み込みを越え、端末(SessionView)は webview に在って消える。pty_id は spawn の時に作られ、起動の戻り値として一度だけ画面へ渡る——消える側にしか無い id である。行は views に見つからないから ▶️ を出し、▶️ は席が埋まっているから拒否される。入口も出口も無い(実機観察、2026-08-24)。

席の台帳が起動そのものの記録を持つ。 pty_id に加えて、開始時刻・起動コマンド・作業ディレクトリを持つ。アカウント id から pty_id への対応表は初めから台帳に在り、seated_accounts が鍵だけを返していた——在るものを渡していなかった。残り三つを台帳へ置くのは、アカウントが稼働中でも編集できるためである。アカウントから読むと、いま走っているものではなく、起動の後に書き換えられた値を出すことになる。

画面は席を見て端末を作り直す。 席を持つ(トピック, アカウント)の組に端末が無ければ、その pty_id で pty-data-{id} と pty-exit-{id} を購読し直す。出力も入力も終了も戻る。席を強制的に空ける経路は作らない——プロセスが在るのに席だけ空けば二重起動が通る(上記「一つのアカウントは一つの部屋に一席まで」)。台帳はアプリの記憶であるから、アプリの再起動の後には稼働中の席が残らない。webview の再読み込みに加え、MCP の開始・再起動後も同じ採用経路を使う(#415)。同じ組に古い PTY の端末が残っていれば新しい PTY の端末へ置き換える。

起動が飛んでいる最中に再読み込みが落ちた場合、その席には繋ぎ直す先がまだ無い(席は確保済みで pty_id は未定)。行の ▶️ を押すと台帳を読み直し、その時点で起動が済んでいれば端末が戻る。拒否は読み直しの後に出す。

線はスクロールバックで引く。 過去の出力は消えた xterm のインスタンスに在り、取り戻す経路が無い。再読み込みから繋ぎ直しまでの出力も同様に落ちる。採用した端末は、接続より前の出力を復元できないことを 1 行目と状態行で知らせる。webview の再読み込み後は、再読み込みの前と同じ開閉に診断ペインを戻し、MCP の開始・再起動ではペインの開閉を保つ(#415、#454)。#454 までは、再読み込みの後は開閉にかかわらずペインを開いていた。開いていたのは下の 1 行を見せるためだったが、その 1 行は実機では読めず(下記)、同じ告知は状態行に出ているため、閉じていた面を開く理由にならない(Master「再起動しない反映時にはなぜか端末が開いちゃう」、2026-10-18)。開閉は sessionStorage(pullcept.diagnostics-open)に置き、何も無ければ閉じたものとして扱う。空の端末が「起動中」と並ぶと、何も出力していないセッションに読める——落ちる直前の出力を読んで判断する形(「診断面」、#57)は、その 1 行が無いと成り立たない。

ただし、その 1 行は実機では読めなかった(2026-08-24、AI が操作)。繋ぎ直した直後に Claude Code が全画面を描き直すため、書いた行はその再描画に上書きされる。告知そのものは状態行にも出ており、そちらは残った——「〈名前〉 の端末に繋ぎ直しました。再読み込みより前の出力は残っていません。」である。情報は伝わっているが、伝えているのは端末の 1 行目ではない。以前ここに「告知が端末の 1 行目で読める」と読める書き方をしていたのは実態と違う。行そのものは書いたまま残す——全画面を描き直さない CLI ではその位置で読めるためであり、消せばそちらで失われる。 #415 から現在の告知は「稼働中の端末に接続/接続より前の出力は復元されません」とし、再読み込みが無い MCP 操作にも同じ意味で使う。

繋ぎ直した端末は、その席のトピックの下に置く(#141)。 画面に出ていないトピックの端末は隠したまま作り、そのトピックを開いたときに出る。

状態の語は「起動中」のままである。 セッションは実際に走っており、別の語を当てれば走っていることのほうを疑わせる。欠けていたのは語ではなく、出力が切れた事実の告知である。その置き場は端末のバッファ自身——失われた出力が在ったはずの位置——であって、行の note ではない。この置き場の判断は、上の実機の結果を受けても変えない。実機で読めなかったのは Claude Code が全画面を描き直すためであり、置き場が誤っていたためではない。

既存設定からの移行

config.json の tabs は accounts として読む。id・名前・起動コマンド・起動オプション・作業ディレクトリはそのまま持ち越し、読み手の居なかった cli_kind は未知フィールドとして落ちる(#17)。色はまだ宣言されていない状態になる。次回の保存で accounts として書き戻す。失われて困る値(作業ディレクトリと起動オプション)は形が変わっていない。

種別だけは手続きを持つ(#156、決定4)。kind が当時の一種類(ai)であるアカウントと、kind を持たないアカウントが対象であり、宣言済みの種別はそのまま残る。起動コマンドから見分ける——claude なら Claude Code、codex なら Codex CLI、それ以外は CLI(汎用) である。判定は読み込んだ JSON の上で、型の付いた設定になる前に行う。答えが別の欄(command)に依っており、serde は一つの欄を決めている最中にもう一つを見られないためである。当時の値を移行専用の変種として残す形は採らない——書かれることの無い変種は、以後すべての読み手が抱える状態になる(画面側の同じ列挙も含めて)。判定そのものは crates/mcp-config/ に置き、テストの対象とする(「テストの配置」)。

人間の種別の旧名 user は admin として読む(#192)。こちらは判定を持たない改名であるから、読み込み時の別名(serde の alias)で足り、JSON の上の手続きは要らない。保存は admin で書き戻す。旧名は読むだけの綴りであって変種ではなく、画面側の列挙にも現れない——画面へ渡る設定は既に admin として型が付いている。

再開コマンドは、既定と同じなら捨て、違うなら残して種別を CLI(汎用) にする(決定6)。Claude Code は戻り方を作法として持つため、同じことを言っている行は持ち物ではない。違うことを言っている行は持ち物であり、それを表示する種別は汎用のほうである——読まれも見えもしない欄に人の書いた行を残さない。

起動オプションからは --session-id を取り除く(決定3)。Claude Code の席ではこのフラグを種別が載せるため、欄に残っていれば同じフラグが二つ並ぶ。欄はその人のものへ戻る。

キャラクターを持たない既存の config.json は、宣言なしとして読む。これも fallback ではなく、その欄が無かった当時の起動が実際にしていたこと——作業ディレクトリの settings.json が指す既定で立つ——そのものである。

mcp-servers.json のエントリにはアカウントを作る(#193)。種別の手続きの後、同じ読み込みの中で、mcp アカウントを持たないエントリそれぞれに一つ作る(「ローカル MCP サーバ」)。書き戻すのは次回の保存であり、それまでは読むたびに同じ id のアカウントが作られる。config.json が無いときも、既定のアカウントにそれを足したものを返す。

mcp は作るときにフォームで宣言でき、手で足したエントリにはアプリが宣言する(#200)。 部屋も画面も推測しないという点は他の種別と同じである。フォームで宣言すれば、そのときにエントリが書かれる(「ローカル MCP サーバ」)。手でファイルへ足したサーバは、エントリとして書かれた時点で、アカウントを作る以前にその素性が決まっており、宣言する者はアプリである。どちらも後から種別は変えられない。start_session は admin と同じくこの種別を拒否する。

画面の本人のアカウントだけは、config 側では移行できない。元の値(名前と色)が webview の localStorage にあり、Rust 側からは見えないためである。画面が起動時に、種別 admin のアカウントが無ければ作る。

.mcp.json の登録単位

.mcp.json の登録鍵はアカウントと部屋の組ごとに分ける。名前は pullcept-room-<slug>-<hash> であり、アカウント id と部屋(トピック)の id だけから決まる。slug はアカウント id から作り、hash は二つの id の組から作る。

部屋を鍵に入れるのは、一つのアカウントが同時に複数の部屋に居られるためである(#141、決定4)。 二つのトピックで同じアカウントを起動すると、どちらもそのアカウントの作業ディレクトリへ書く。鍵がアカウントだけであれば、後の起動が前の起動のエントリ——部屋の id を含む——を、前のセッションが走っている間に書き換える。#40 と同じ形が一つ軸をずらして現れる。部屋を URL(パス)で分ける形は採らない。起動時の「アドレスが違う登録は死んだ登録」という掃除(下記)が他の部屋の登録を消すためである。同じ実行の部屋はアドレスを共有するため、鍵に部屋を入れればその掃除は変えずに済む。

鍵が 1 つに固定されていたときは、同じ作業ディレクトリを指す 2 セッションが同じ鍵を上書きした。2 つめの起動が 1 つめの名前・色・部屋アドレスを書き換え、1 つめのセッションがまだ動いている間にその識別が消える(#40)。

鍵を名前から導出しない理由は改名である。名前が編集できるようになったため、名前由来の鍵は編集のたびに動く。稼働中のセッションが起動時に指した登録は古い鍵の下に取り残され、そのセッションを持つ CLI のコマンドラインには古い server: タグが載ったままで、後から書き換える経路が無い。id は不変であるから、改名の費用がゼロになる——名前を編集可能にできるのはこの性質による。

鍵を起動ごとの id にもしない。それでは起動のたびに 1 つ増え、ユーザーのリポジトリのファイルが際限なく伸びる。アカウント id と部屋の id の関数にすることで、同じトピックで同じアカウントを立て直したときは同じエントリを使い回す。トピックを渡り歩くと実行中はトピックの数だけ増えるが、次の実行の最初の起動で、アドレスの違う死んだ登録として掃除される。

slug は読みやすさのためだけにあり、ASCII 英数字と - に落とす。一意性は hash が担う。id は不透明であるため、名前を鍵にしていたときより読みにくい——どのアカウントのエントリかは、そのエントリ自身の PULLCEPT_AGENT_NAME から、どの部屋のエントリかは PULLCEPT_ROOM_ID から読む。読みやすさより同一性を採る。鍵が読みにくいのは 1 回の参照で済み、鍵が動くのは登録が行方不明になることであるから。

登録のたびに、pullcept-room で始まり PULLCEPT_ROOM_URL が現在の部屋アドレスと異なるエントリを削除する。部屋は起動ごとに新しいポートを取るため、それらは前回起動の残りであり、どのサイドカーも到達できない。残しておくと、この作業ディレクトリで CLI を起動するたびに死んだポートへ再試行するサイドカーが 1 つずつ増える。現在のアドレスを持つエントリは同一起動の別セッションであるから残す。PULLCEPT_ROOM_URL を持たないエントリは、名前が前方一致しても他人のものとして触らない。

セッションは自分以外の部屋の登録を起動しない

.mcp.json はプロジェクト単位の設定であり、CLI はそこに在る有効なサーバを全部起動する。登録鍵がアカウントごとに分かれているのは上記の通り識別を守るためであるが、作業ディレクトリを共有すると、その分かれた登録が同じファイルに並ぶ。したがって 1 つのセッションが他のアカウントのサイドカーまで起動する。起動されたサイドカーは、起動した者ではなく登録された側の身元を名乗って部屋に入るため、名簿に同じ参加者が二人現れる(#103、2026-08-26 に実機で観測)。

共存できるようにしたことがそのまま全部起動される条件になっている。#40 の鍵の分離を戻す形は採らない——戻せば識別の上書きが復活する。

起動する行に --settings の disabledMcpjsonServers を載せ、そのセッションが起動しない部屋サーバを名指しする。 名指すのは、この起動自身の一本(このアカウントのこの部屋のエントリ)を除く、作業ディレクトリの .mcp.json に在る pullcept-room 接頭辞のキー全部である。同じアカウントの別の部屋のエントリもここに入る(#141)——止めなければ、一つのセッションが別のトピックの席まで起動する。

enabledMcpjsonServers は採らない。それは起動を絞らない(実測、2026-08-26。使い捨ての probe で、起動時にマーカーを書くだけのサーバ 2 本を置いて判定した)。自分の一本だけを enabledMcpjsonServers に挙げても両方が立ち上がる——承認の可否を持つ鍵であって、起動の可否ではない。disabledMcpjsonServers は名指した側だけを止める。どちらも --settings のインライン JSON で渡って効く。

--mcp-config で設定を手渡す形も採れない。sidecar は名前で登録され、その名前で承認されていなければならない(部屋の入力路が channel だった間は、渡した設定を channel 側が解決しなかった。「成立条件」)。起動のたびに他アカウントのエントリをファイルから削る形も採れない——走っている側の登録が消え、#40 が防いだ状態に戻る。

名指しの出所は .mcp.json の実物であり、アカウントの一覧ではない。 アプリから削除されたアカウントの登録がファイルに残っている場合や、別の経路で書かれた pullcept-room 接頭辞の登録は一覧に出てこない。CLI が読むのはファイルの方である。読む時点は登録を書く前であり、判定は上記の掃除と同じ述語を通す——書いた後のファイルについて答えながら、書く前に答える。そうすることで、起動オプションの --settings との併記による拒否が、何も書かないうちに済む。拒否した後にこのアカウントの登録がファイルへ残れば、それこそがこの節の不具合そのものになる。

素通しの条件が変わる。 キャラクターを宣言しないアカウントは --settings が一切付かずに起動していた(上記「アカウントはキャラクターを持つ」)。同じディレクトリに他の部屋の登録が在る場合は、キャラクターの有無に関わらず --settings が要る。素通しは「他に止めるべき登録が無いとき」である(#143 以降は承認が常に載るため、素通しは自前の --settings を持つ行に限られる。下記「セッションは自分の部屋の登録を承認して起動する」)。したがって起動オプションが自前の --settings を持つアカウントの拒否も、キャラクターを宣言している場合に加えて、止めるべき登録が在る場合に広がる。拒否の理由はどちらであるかを文面で言い分ける。

走る行の表示(上記「アカウントのモーダル」)にも同じ名指しが載る。作業ディレクトリの欄を共有ディレクトリへ向けた瞬間に --settings が現れることは、決定する前に見えている必要がある。表示は欄が指すディレクトリを読むため、まだ存在しないディレクトリを打っている途中は何も名指さない——起動の側はそのとき改めてファイルを読む。

実機で本数が落ち、名簿の重複が消えた(2026-08-26、AI が操作。「実機で確認済み」)。共有ディレクトリで 2 セッションを順に起こし、後発の行に先行の登録の名指しが載っている。部屋型サイドカーは各セッション 2 本であり、修正前の Lin 側 3 本から 1 本落ちた。名簿の AI は 2 行、Lay と Lin が一人ずつである。止める一覧を持つ側の部屋の往復も生きている。 自分の登録まで止めてしまえば沈黙として現れる経路であり、本数と名簿とは別の軸である。

名指しは時点で閉じ、時点がずれれば閉じない(#208)。 名指しの一覧は起動を組み立てるときにファイルから読み、CLI がファイルを読むのはその数秒後である。その間に別のアカウントの起動が自分のエントリを書き足すと、先の起動の一覧にそのエントリは無く、先の CLI はそれも起動する。起動されたサイドカーは登録された側の身元で部屋に入り、名簿に後から起動したアカウントが二人現れる(2026-09-28、Master が実機で観測。共有ディレクトリで Claude Lay の起動が終わる前に Claude Lin を起動すると Lin の行が二つ出、待ってから起動すると出ない)。

サイドカーが、自分を起動した CLI がどのアカウントとして起動されたかを確かめ、違えば部屋に入らない。 アプリは CLI のプロセスの環境変数 PULLCEPT_LAUNCHED_AS に、その起動のアカウント id を置く(crates/mcp-config の LAUNCHED_AS_ENV)。これは .mcp.json のエントリではなく起動そのものに付く——エントリはファイルの事実であり、どの CLI が読んでも同じであるが、どの CLI が読んでいるかは起動だけが知っている。CLI は stdio の MCP サーバへ自分の環境を受け継ぎ、エントリの env はそれに足される(2026-09-28、Claude Code 2.1.283 で --mcp-config 経由の起動を測った。親の変数とエントリの変数の両方がサーバに見えた)。サイドカーは受け継いだ PULLCEPT_LAUNCHED_AS と、自分のエントリの PULLCEPT_ACCOUNT_ID を比べ、一致しなければ部屋ソケットを開かない。時点ではなく身元で閉じる。起動を直列化する形(前の起動の完了を待たせる)は採らない。

  • 断ったサイドカーは道具も作法も出さない。 起動した CLI は別のアカウントのもので、自分のサイドカーを既に持っている。作法を渡せばそのセッションに答えるべき二つめの名前を渡すことになり、二つめの say_to_room は取り違えうる。終了はしない——終了すれば CLI はそれを失敗したサーバとして出すが、登録は意図どおりに働いている。理由は stderr に room socket: not joining: … として残す。名簿からは、入らなかったサイドカーと届かなかったサイドカーの見分けが付かないためである。
  • アプリが起動していない CLI が読んだ登録も部屋に入らない。 共有ディレクトリで手で claude を起動した場合などである。そのプロセスには PULLCEPT_LAUNCHED_AS が無く、アプリが書いたエントリは必ずアカウントを名指すため、一致しない。以前はそうした CLI も登録された側の身元で部屋に入っていた。アカウントを名指さないエントリを、アカウントを持たない起動が読んだ場合は一致とみなし、アカウントを持たない接続として入る(「部屋プロトコル」、#59)。アプリが書く登録はこの形にならない。
  • 名指しは残す。 名指しは CLI が起動するサイドカーの本数を減らす。この突き合わせは、名指しで閉じない競合の分だけを閉じる。
  • 部屋のアドレスが無いサイドカーには問わない。 どちらにせよ部屋に入らず、アドレスが無いことをその理由として言う。

一アカウント一席(RoomSeats)はアプリ内の話であり、他プロセスが同じ id で入ってくる経路を塞ぐものではない。この節はその経路のうち、このアプリ自身が作っていた一本を閉じる。

セッションは自分の部屋の登録を承認して起動する

起動する行の --settings に enabledMcpjsonServers を載せ、この起動自身の一本(このアカウントのこの部屋のエントリ)を承認する(#143)。起動の行でも再開の行でも同じく載る。

CLI は .mcp.json のサーバのうち承認されていないものを、セッションの開始前に「New MCP server found in this project」の確認で止める。登録鍵は部屋ごとであるから(上記、#141 の決定4)、トピックを開くたびに CLI がまだ一度も訊かれていない名前になる。鍵をアカウントごとに戻す形は採らない——戻せば一つのアカウントが二つのトピックに居るときに互いの登録を上書きする。

初回の起動で確認が出ず、再開で出た理由は起動の行の違いである(2026-09-17、CLI 2.1.273 の同梱コードと実環境の設定を読んで特定。実機での再現はしていない)。CLI は承認されていないプロジェクトのサーバを、--dangerously-skip-permissions で起動しており、かつユーザー設定が skipDangerousModePermissionPrompt を持つときに限り承認済みとして扱う。当時、起動オプションはこのフラグを持ち、再開コマンド(claude --resume {session_id})は持たなかった。承認がモードに乗っていただけであるから、行の側で承認して二つの行を同じ状態にする。#167 以降は Claude Code の再開の行にも起動オプションが載るため(上記「再開コマンド」)このフラグの差は無くなったが、承認はモードに依らず行の側で持つ——起動オプションの欄が落ちる起動(#154)もあり、フラグを書かないアカウントもある。#142 より前は鍵がアカウントごとであり、一度承認した名前が settings.local.json に残っていたため、再開でも表に出なかった。

enabledMcpjsonServers は承認の鍵であって起動の鍵ではない(上記、実測 2026-08-26)。ここではまさに承認の鍵として使う。起動を絞るのは引き続き disabledMcpjsonServers である。

ファイルには書かない。 確認に答えれば CLI は作業ディレクトリの settings.local.json へ名前を書き足す。部屋ごとの名前でそれを続ければ、トピックの数だけ鍵が増える——共有ディレクトリを増やさないという上記「アカウントはキャラクターを持つ」の判断に反する。インラインの承認はその起動にだけ効く。

拒否は覆さない。 確認で「Continue without using this MCP server」を選ぶ(既定の選択であり、Esc も同じ)と、CLI はその名前を settings.local.json の disabledMcpjsonServers へ書き、承認より先にそれを見る。そのトピックでのこのアカウントの席は、行に承認を載せても部屋へつながらない。消すのは利用者のファイルであり、このアプリは書き換えない。

自前の --settings を持つ行は、他に載せるものが無ければ素通しのままにする。 承認のために --settings の併記の拒否を広げない——拒否はキャラクターか止めるべき登録が在るときに限る(上記)。そういう行は自分の settings で承認するか、確認に答える。

セッションは自分の様子をステータスラインで知らせる

起動する行の --settings に Claude Code の statusLine を載せ、その席が何を使って走っているかをアプリへ知らせる(#155)。Claude Code の席では、パネルの末尾の五欄はこの合図だけから出る(Codex CLI の席は下記「Codex の席は自分の様子を rollout で知らせる」。「参加者パネル」)。

出所はステータスラインの JSON であり、端末の出力ではない(決定1)。Claude Code はステータスライン用のコマンドを走らせるとき、そのセッションの JSON を stdin で渡す。要る五つは全てそこに在る(公式 docs code.claude.com/docs/en/statusline、2026-09-17 に読んだ。実機の CLI では計っていない)。

欄 JSON の出所
モデル model.display_name。無ければ model.id
エフォート effort.level(low / medium / high / xhigh / max)
5 時間 rate_limits.five_hour.used_percentage
週次 rate_limits.seven_day.used_percentage
コンテキスト context_window.used_percentage
5 時間・週次のリセット(#306、#320) rate_limits.five_hour.resets_at / rate_limits.seven_day.resets_at(Unix 秒。同 docs、2026-10-07 に読んだ)

欄はどれも欠けうる。 rate_limits は claude.ai の Pro / Max の契約でなければ来ず、契約が在っても最初の API の応答までは来ない。effort はその欄を持たないモデルでは来ない。context_window.used_percentage はセッションの序盤で null になりうる(同 docs)。欠けているものは「—」であり、空欄でも 0% でもない。 0% と、まだ知らないことは別である——「観測していない状態の語は許されない」(「走っているアカウントが何をしているか」)と同じ線である。

受け口は部屋のソケットと同じ一本である(決定2)。部屋のポートへ POST し、部屋は WebSocket の upgrade(GET)で始まるため最初のバイトで二つの呼び手を分けられる。別のポートを開かないのは、起動の行が既に部屋のポートを運んでいるためである——第二のアドレスは、起動のたびに配り、持ち、返す対象が一つ増えることになる。

席はアドレスが名指し、アドレスはクエリを持たない(#152)。ステータスラインが受け取る JSON が運ぶのは CLI のセッション id であって、このアプリが席を鍵にしている(トピック, アカウント)ではない。id を置く先を種別も起動オプションも持たないアカウントには、セッション id がそもそも配られない(「トピック」の決定4B、#156)。起動はどちらも知っているため、起動が書く。

そのアドレスは --settings の JSON の文字列の中身として起動の行に載り、Windows ではその行が cmd.exe /C を通って CLI へ届く。引数は自前の " を持つため、引用は各々を \" に直して全体を " で包む——cmd.exe は \ を逃がしと読まず、" の数だけを数える。包んだ側の " が偶奇を反転させるため、JSON の文字列の中身は cmd.exe から見て引用の外側に立つ。そこでの & はコマンドの区切りであり、?room=…&account=… は起動の行をその位置で終わらせ、残りを別のコマンドとして走らせた——#151 が出したのはこの形であり、アカウントは一つも起動できなかった。したがってこのアドレスには cmd.exe が反応する文字を置かない:区分が / を担い、残りは percent encode が %XX にする。% だけは encode 自身のものとして残る(%NAME% の展開。未定義の名はコマンドラインでは素通しであり、id は uuid で encode の対象にならない。両側とも未計測)。

トークンはヘッダで確かめ、行には載せない。 Authorization: Bearer <部屋のトークン> であり、値はスクリプトが起動した子プロセスの環境(PULLCEPT_ROOM_TOKEN)から読む。ループバックは同じ機械の別プロセスからも届くため、確かめずに受ければ機械上の何からでも五欄に好きな値を——「制限中」を含めて——立てられる。環境に置くのは、走る行が画面に出るためである(上記「アカウントのモーダル」)。

送られてきた中身は読む。 値そのものが目的であるためである。代償は欄名への依存である——欄名は CLI のものであり、CLI がどれかを改名すればパネルの一行が一緒に消える。端末の出力を読まないこと(#82)は動いていない。読んでいるのは CLI が構造化して寄越す報告であって、端末に出た文字ではない。

コマンドの実体は pullcept が持つ 1 本である(決定3)。sidecar/src/status.mjs であり、受け取った JSON をそのまま受け口へ送り、端末には短い 1 行を返す。

  • node <スクリプト> <アドレス> の三語だけを行に載せる。 トークンは載せない——起動した子プロセスの環境から読む(上記「トークンはヘッダで確かめ、行には載せない」。行は画面に出る)。
  • 行に載る語は、途中の三つのシェルが手を出さない文字だけで書く。 --settings の引数は Windows では cmd.exe /C を通り、JSON の文字列の中身は cmd.exe から見て引用の外側に立つ(#152)。そこへ & や ( が出れば行はその位置で終わる——C:/Program Files (x86)/… はまさにその形である。さらに、ステータスラインのコマンド自体を走らせるのは Git Bash(入っていなければ PowerShell)であり、Git Bash は引用の無い \ を逃がしと読むため、パスは / に直して渡す(同 docs)。
  • 許す文字は ASCII の英数字と / : . _ - だけである。 空白も非 ASCII も許さない。引用で包み直す形は採らない——包めば cmd.exe の引用の偶奇がもう一度反転し、以後この行に触れるたびにその計算をやり直すことになる。
  • 通らない語があればステータスラインを載せない。起動は止めない。 スクリプトが在るべき場所に無い場合と、部屋のポートがまだ無い場合も同じ答えである。その席のパネルの五欄は「—」のままになる。#151 が出したのは「行を壊す」形であり、ここはその逆側へ倒す。
  • スクリプトが失敗しても端末のバーは描く。 アプリが閉じていても、JSON が読めなくても、1 行は出す。空のバーは「ステータスラインが壊れている」と読めるだけで、見ている人には理由が出ない。

利用者のステータスラインは置き換わる。 statusLine はリストではない一つの値であるため、--settings の層がそのまま利用者・プロジェクト・ローカルの設定より優先する(公式 docs code.claude.com/docs/en/settings、2026-09-17 に読んだ。実機の CLI では計っていない)。hooks とは併合の規則が違う——hooks は層をまたいで併合され、こちらは上の層が丸ごと勝つ。この行が宣言する hooks は下記「試験中:--settings の http hook が届くか」の二つだけであり、それは利用者の hooks に足されるだけで消さない。したがってその違いがこのアプリに効くのはこちら側だけである。したがってこのアプリから起動したセッションでは、その人が書いたステータスラインは出ない。これはキャラクター(outputStyle)が既に持っている形であり(上記「アカウントはキャラクターを持つ」)、二つを合成する形は採らない——合成するには利用者の設定ファイルを読むことになり、このアプリしか読まない 1 行のために、CLI に合わせて保つものがもう一つ増える。

更新はセッションが動いたときだけである(決定4)。コマンドが走るのはセッションの開始時と、アシスタントの発言が届いたとき、/compact の完了時、許可モードの変更時などである(同 docs、実機では未計測)。止まっている間は最後の値のままである。 セッションが終了しても五欄は残る——この列が答えるのは「何が走ったか」であり、他の欄が終了後も残るのと同じ形である。沈黙で空にする形は採らない:まだ抱えているコンテキストについて「使っていない」と言うことになる。

リセットの文言だけは、報告ではなく時計で数える(#306、#320)。 5 時間と週次の行の 1 行目、% の左に出す(Claude デスクトップと同じ並び)。5 時間は残り時間を「4時間10分後にリセット」の形で出し、分が変わるたびに書き直す。分は切り上げで数え、窓がまだ動いている間に「0分」とは言わない。1 日以上残る間は「3日5時間後にリセット」と分を落とす。週次は残り時間ではなく、リセットの現地時刻を曜日つきで「2:00 (月) にリセット」の形で出す(時は先頭に 0 を付けず、分は 2 桁、曜日は日本語の一文字)。この文言はリセット時刻を過ぎて消えるときにしか変わらない。リセット時刻を過ぎたら、その行は消す。 窓は始め直しており、過ぎた時刻を出し続けない。使用率の値は上の決定4のとおり次の報告まで残す——Claude Code はリセット時刻を過ぎた窓を JSON から落とし、その時点でステータスラインを走らせ直す(同 docs、2026-10-07 に読んだ。実機では未計測)。リセット時刻が届いていない窓と、「—」の行には何も出さない。秒の読み方は mcp_config::epoch_seconds であり、Codex の席と共通である。「制限中」の札はこの行と関わりなく今のままである。

席自身も 5 時間と週次の値を読める(#364)。 部屋の道具 my_usage が、アプリの受け取った時刻をつけて返す。Codex の席も同じである(「席が自分の利用上限を読む」)。

「制限中」もこの五欄から出る(#161)。 5 時間と週次の二つが、パネルの行であると同時に note の「制限中」の出所である(「走っているアカウントが何をしているか」)。#149 が起動の行に置いていた StopFailure hook はこのために撤去した——同じことを二つの経路で言っていたのではなく、hook の経路は一度も発火を観測できないまま残っていた(Master、2026-09-18)。

再読み込みは越えない。 報告は届いた時点の画面が持つ。走ったままのセッションを拾い直した画面は次の報告まで「—」であり、note も「制限中」を出さない。宛先の記録(#82)と同じ形である。

自前の --settings を持つ行には載らない。 承認(#143)と同じ例外の内側であり、ステータスラインのために併記の拒否を広げない。その席は五欄が「—」のままであり、「制限中」も出ない。

種別が Claude Code の席にだけ載る(#156、決定5)。statusLine の書き方は Claude Code のものであり、作法を持たない種別の行へ載せる正しい形をアプリは持たない。CLI(汎用) の席の五欄は「—」のままになる。Codex CLI の席は statusLine を持たず、下記の rollout から同じ五欄を出す。

Claude Code の席は自分の様子を hook で知らせる(#331)

起動の --settings に http 型の hook を載せ、ツール・許可待ち・サブエージェントの始まりと終わりをアプリへ送らせる。 アプリはそれを席ごとの様子にまとめ、app-server 方式の Codex の席と同じ seat-activity の事象で画面へ送る(「走っているアカウントが何をしているか」)。端末の文字は読まないため、#82 の判断は動いていない。#325 の試し(PreToolUse と SubagentStart を載せて届いたことを logs\hook-probe.log に書く)で、PreToolUse が実機の席から届くことを確かめた(2026-10-08)。この形に置き換え、試しの記録は外した。

  • 載せる hook は八つである。 PreToolUse・PostToolUse・PostToolUseFailure(ツール)、PermissionRequest(許可待ち)、SubagentStart・SubagentStop(サブエージェント)、Stop・UserPromptSubmit(ターンの区切り)。matcher は付けず、すべてのツール・すべての種類のエージェントで発火する。
  • 形は #151・#325 と同じである。 type: "http"、timeout: 5(PermissionRequest だけ 190、下記)、ヘッダは Authorization: Bearer ${PULLCEPT_ROOM_TOKEN} であり、トークンは allowedEnvVars で CLI が子プロセスの環境から解決する。行に載るのは変数の名前だけで、値は載らない。
  • 宛先は部屋のポートの /hooks/activity/<出来事>/<トピック>/<アカウント> である。 出来事の種類はパスで分ける。席の名指し方と、cmd.exe が反応する文字を置かないことは、上記のステータスラインのアドレスと同じである。宣言していない出来事の名前のパスは 404 である。
  • CLI は返事を待つ。 http 型の hook は裏で走らせられない(公式 docs code.claude.com/docs/en/hooks、2026-10-07 に読んだ)。ツールを使うたびにこのアプリの返事を待つことになるため、timeout を 5 秒に絞り、アプリは本文を読んで様子を変えたらすぐ 200 {} を返す。{} は何も決めない返事であり、ツールの実行も許可の画面も、hook が無いときと同じに進む(同 docs「HTTP response handling」、2xx で JSON の本文は通常の出力として読まれる)。様子を変えてから答えるのは、CLI がツールの終わりを送るのはその始まりの答えを受けた後だからである——始まりと終わりの順が入れ替わらない。
  • PermissionRequest だけは部屋の答えを待つ(#336)。 部屋にカードを出し、画面の人が押すまで最長 180 秒、接続を開けたまま待つ。そのため、この hook だけ timeout を 190 秒にする(既定の 600 秒より十分短く、待つ時間より少し長い)。待つ間も端末の許可の画面は出ており、待つ時間が過ぎれば {} を返す。決め方は「許可の確認に部屋から答える」。
  • 本文から読むのは五つの欄だけである。 hook_event_name・tool_use_id・tool_name・agent_id・agent_type。Stop では加えて background_tasks の各要素の type だけを読む(下記)。tool_input・tool_response・prompt・last_assistant_message などの中身は読まず、保存もしない。hook_event_name がパスの出来事と違う本文は、まったく読まない。欄の名前は同 docs(2026-10-08 に読んだ)による:共通の欄(hook_event_name、サブエージェントの中でだけ付く agent_id と agent_type)、PreToolUse の tool_name・tool_use_id、PostToolUse・PostToolUseFailure の同じ二つ、tool_use_id を持たない PermissionRequest、SubagentStart・SubagentStop の agent_id・agent_type、Stop の background_tasks。サブエージェントの中のツールの hook にも agent_id と agent_type が付く(同 docs「Hook locations」)。
  • PermissionRequest は、カードのためにもう三つ読む(#336)。 tool_input・mcp_server.name・permission_suggestions である。読んだ中身はカードの事象として画面へ渡すだけで、記録にも端末にも出さず、待ちが終われば捨てる。席の様子(札と一行)には今までどおりツールの名前しか使わない。
  • アプリは本文を 4 MiB で読み切りをやめる(HOOK_BODY_MAX)。大きなファイルの Write の tool_input や、大きな結果の tool_response はここで切れ、JSON として読めなくなる。読めないときの扱いは「Claude Code の席の様子の決め方」。
  • 席ごとの様子は起動ごとに持つ。 鍵は(トピック, アカウント)で、その起動の端末の id と一緒に持つ。同じ席を起動し直せば、新しい端末の id で空の様子から始まり、前の起動の残りは言わない。走っている端末の無い席への hook は持たない(src-tauri/src/hook_activity.rs)。何を言うかは crates/mcp-config/src/hook_activity.rs(tauri を持たずテストされる)が決め、画面に出す語が変わったときだけ事象を送る。事象の形は Codex の席と同じで、connected は常に true、thread_status は常に null である——hook の席には切れる接続も、言えるスレッドの様子も無い。
  • hook のほかに一つだけ、人が席の端末に打ったキーを使う。 許可した後のツールは終わるまで hook を送らないため、本体の許可待ちは、答えのキー(Enter・Esc・数字・y / n)が画面の端末の欄から打たれたところで閉じる(pty::write_pty から HookSeats::typed)。キーで読むのはそれが答えのキーかどうかだけで、打った文字は持たない。部屋の発言をアプリが打ち込む経路は通らない。決め方は「Claude Code の席は hook で様子を言う」。
  • 載る席はステータスラインと同じである。 種別が Claude Code の席だけで、自前の --settings を持つ行・部屋のポートが無い場合・スクリプトのパスが行に載らない場合には、ステータスラインと一緒に載らない(mcp_config::SeatReport)。disableAllHooks が効く環境でも届かない。届かない席は今までどおり四つの語を出す。
  • 利用者の hooks は消さない。 hooks は設定の層をまたいで足し合わされる(同 docs)。

Codex の席は自分の様子を rollout で知らせる(#283)

Codex CLI はステータスラインのコマンドを持たない。代わりに、会話記録の rollout へ同じ値を書く。 アプリはその席の rollout $CODEX_HOME/sessions/YYYY/MM/DD/rollout-<時刻>-<native id>.jsonl を読み、Claude Code の席と同じ session-stats として画面へ渡す。パネルの表示と「—」の読み方は共通のままである(SessionStats::from_codex)。「制限中」だけは使用率から読まない(下記「Codex の席が利用上限で止まったとき」、#294)。起動の行には何も足さない。

欄 rollout の出所
モデル turn_context 行の payload.model
エフォート turn_context 行の payload.effort
5 時間 event_msg / token_count 行の payload.rate_limits のうち window_minutes が 300 の窓の used_percent
週次 同じく window_minutes が 10080 の窓の used_percent
5 時間・週次のリセット(#306、#320) 同じ二つの窓の resets_at(Unix 秒)。使用率と同じ窓から読み、同じ行で置き換える
コンテキスト 同じ行の payload.info.last_token_usage.total_tokens と payload.info.model_context_window から、下記の Codex の式で出す

コンテキストは Codex の /status と同じ式で出す(#285)。 単純な割り算(total_tokens ÷ model_context_window)は Codex の表示と合わない——2026-10-05 の実機で、パネルが 11% のとき /status は 93% left (29.1K used / 258K) であった。Codex は使用量と上限の両方から基準量 12000 トークンを引き、残りの割合を整数に丸めて出す(openai/codex の codex-rs/protocol/src/protocol.rs の percent_of_context_window_remaining、2026-10-05 に読んで確認)。アプリは同じ式で残りを出し、パネルには 100 − 残り を使用率として出す。上の例では 7% である。

  • 上限が 12000 以下なら、残りは 0%。
  • 有効な上限 = 上限 − 12000。
  • 使用量 = total_tokens − 12000(0 を下回れば 0)。
  • 残り = 有効な上限 − 使用量(0 を下回れば 0)。
  • 残り% = 残り ÷ 有効な上限 × 100 を 0〜100 に収め、整数に丸める。

12000 は Codex の内部値であり、公開仕様ではない。 Codex の版で変わりうる。変われば、パネルと /status の値は再びずれる。Claude Code の席が Claude Code の出す使用率をそのまま出すのと同じく、各 CLI が自分で見せている数字を出すための式である。

行の形は Codex の公開仕様ではない。 2026-10-05 に Codex CLI 0.160.0 の実セッションで観測した形だけに基づく。JSON でない行・知らない種類の行・途中で切れた行・欄の欠けた行は、読めない欄を変えないだけで、例外で止まらない。一度も読めていない欄は「—」である。turn_context はそのターンの写しとして二欄とも置き換え、欠けた方は「—」になる。token_count は info と rate_limits の片方だけを運ぶことがあり、運ばなかった方は前の値のままである。5 時間と週次は primary / secondary の位置ではなく窓の長さで見分け、limit_id が codex 以外の枠の値は採らない——別の枠の数字をこの席の 5 時間・週次として出さないためである。

読むのは末尾の追記分だけである。 読んだ位置を持ち、次はそこから読む。ファイル全体を読み直さない。改行の届いていない行は次の回まで待つ。64 KiB を超える行(道具の出力の行は数 MB に達する)は持たずに読み飛ばす。読む五欄の行は 2 KiB 前後である。

再開では、起動時点のファイル長から読む。 前回の実行が書いた値を今回の値として出さない。Claude Code の席が次の報告まで「—」であるのと同じ形であり、再開した席は最初のターンまで「—」である。

新規起動では、id とファイルの出現を待つ。 native id は最初の SessionStart の callback(上記「Codex CLI の起動と再開」)で届き、rollout の作成はそれより遅れることがある。アプリは起動ごとに一本の見張りを立て、1 秒ごとに席の native id とファイルを探し直す。id が無い・ファイルが無い・読めない、はどれも「今回は無し、次の回に探す」であり、起動も会話も止めない。ファイルが 1 分見つからなければ、探す間隔を 10 秒に落とす(sessions/ の走査を毎秒続けないため)。見張りは、その起動が席を離れたか PTY が終わった時点で終わる。五欄は最後の値のまま残る(上記「更新はセッションが動いたときだけである」と同じ)。

CODEX_HOME は会話の実在の判定と同じ答えを使う。 アカウントの環境変数、アプリの環境変数、~/.codex の順であり、相対パスは起動ディレクトリに対して解決する。

Codex の席が利用上限で止まったとき(#294)

Claude Code の席では手作りの hook がしていること(止まったら知らせ、解除後に再開させる)を、Codex の席ではアプリが担う。 Codex には「エラーで止まった」ときの hook が無い(#290)。Claude Code の席の挙動は変えない——以下はどれも Codex CLI の席だけに働く。

止まったことは rollout から読む。使用率からは読まない。 利用上限で止まったターンは、rollout に event_msg / task_complete の行を書き、その payload.error.codex_error_info が usage_limit_exceeded である(2026-10-05、0.160.0 の実セッションで観測)。アプリは五欄と同じ見張り(上記)でこの行を読み、その席を「止まった」とする。rollout の使用率は止まった席で 99.0 のまま動かず(#290)、100% を待つ形では「制限中」が出なかった。パネルの「制限中」はこの判定から出し(session-stats の limited)、Codex の席では使用率の 100% を見ない。

止まったら部屋に一通知らせる。 発言者は Pullcept(自動通知)(アカウントを持たない)、札の role は app である(「発言は端末へ入る」)。文面は止まった席の名前と解除予定(この PC の現地時刻、10月5日 22:45 の形)。解除予定は、止まった時点で一度だけ Codex の app-server に account/rateLimits/read を聞き、ordinaryUsageAllowed: false の答えの resetsAt を採る。聞けなければ rollout の最後の token_count の resets_at を採り、どちらも無ければ「分かっていません」と書く。どちらも、使用率がいちばん高い窓(二つが同じなら遅いほう)の解除時刻である。この知らせは止まった席の端末へは打たない——打てば走れないターンが一つ増える。止まっている相手に作業を振らないための知らせである。

止まっている間も、部屋の発言はその席の端末へ打つ(#403)。 預からない(#401 で Claude の席から外したのと揃えた)。制限中に打たれた発言は、Codex が会話に user の発言として残し、そのターンが利用上限で終わる——捨てられはしない(2026-10-12、ローカルの rollout で response_item の user の発言の後に usage_limit_exceeded の task_complete が続くことを確認)。代わりに、発言のたびに利用上限で終わるターンが一つ増える。解除後は、席が必要ならトピックの記録を read_room_history で読む。#294 から #403 まではアプリが預かり、受け箱に id を書き、解除のときにまとめの一通を渡していた。Master の観測(2026-10-12)で「制限中に部屋の発言が来ない」ことが問題になり、外した。

解除はアプリが聞いて確かめる。推測しない。 Codex には「解除された」通知が無く、プロトコルは「使用率や解除時刻から回復を推測してはならない」と書く(app-server-protocol/src/protocol/v2/account.rs:333)。アプリは止まっている席についてだけ account/rateLimits/read を聞き、ordinaryUsageAllowed が true と答えたときに限って解除とする。false・null・エラー・応答なし・壊れた応答は、どれも「まだ止まっている」である。止まっていない席には聞かない。

  • 最初に聞くのは解除予定の 30 秒後である。解除予定が分からない、または既に過ぎていれば 1 分後。
  • まだ使えなければ 1 分・2 分・5 分・10 分・15 分と間を空け、以後は 15 分ごとに聞く。答えが先の解除予定を名指せば(5 時間の後に週次で止まった等)、その 30 秒後まで待つ。
  • 止まっている間にターンがエラー無しで終われば(端末で直接打った、または上限を手でリセットした後に部屋の発言が通った)、次の問い合わせを今に繰り上げる。それ自体を解除とはしない。上限を手でリセットしても、誰かが発言すればそのターンが通り、その場で聞き直して解除に気づく(#377 の「発言を契機にする」は、預かりを外した後はこの形で残る。#377 の HELD_GAP は預かった発言の契機だったため無くなった)。
  • app-server に繋がらなければ解除を確かめられず、席は「制限中」のまま、再開の指示は打たれない(「受容したトレードオフ」)。

解除を確かめたら、部屋に一通、席に一通。 部屋へは解除を知らせ、その席の端末へ再開の指示を打つと言う(Pullcept(自動通知)、app。その席の端末へは打たない)。席へは、「利用上限の解除を確認しました。中断していた作業を再開してください。中断していた作業がなければ、そのまま待機してください。」を一度だけ打つ(#389、limit::CONTINUE)。まとめは付けない。この一通の札の message_id は部屋への解除の知らせの id である——席が次の say_to_room の last_seen に使えば、その知らせで弾かれない。席を「制限中」から戻すことと一通を打つことは一つの錠の内で行い、その後に届いた発言は一通の後に打たれる。

打った後の分かれ道(#403、#401 と同じ考え方)。

  • ターンが始まるか終われば、指示への応答である。それ以上は打たない。
  • ターンが利用上限で終われば、まだ解けていなかっただけである。新しい停止として扱い(部屋への停止の知らせも出る)、次に解除を確かめたときにもう一度打つ。
  • どちらも 120 秒無ければ(limit::ANSWER_WAIT、Claude の席と同じ)、利用上限以外の理由(起動時の選択画面、ログイン切れ、信頼の確認など)で止まっているとみなし、部屋に一度だけ「端末を見てください」と知らせる。選択肢に入力が入る危険があるため打ち直さない。その席が後でターンを正常に終えたら、戻ったことを一度だけ知らせる(席ごと、起動をまたいで覚える)。

止まっている席の印(#312)。 止まったことがメモリの中だけにあると、止まった席のセッションが終わるか Pullcept が起き直したときに消え、そのあと上限が外れていても、起き直した席は止まっていない状態から始まるので解除の流れに進まず、再開の指示が届かない(2026-10-07 に実際に起きた)。そこで、止まっている席にだけ、再起動をまたいで残る小さな印を持つ。

  • 置き場所は、トピックの記録と同じ部屋のディレクトリの mailboxes/<トピックの id>.json で、止まっているアカウントごとに一つの項目(値は空の並び)を持つ。トピックを消せば一緒に消える(topic_index::delete)。#403 より前に書かれた、預かった発言の id を持つファイルも、同じ印として読む。
  • 席が止まったときに印を書く。
  • 席を起動したとき、そのトピックとアカウントの印が残っていれば、その席は止まった状態から始まる。部屋への停止の知らせは出し直さない(止まったときに出している)。パネルにはその時点で「制限中」を一度送る。
  • 止まった状態から始まった席は、最初の見張りの一回目で app-server に聞く。解除予定の時刻は待たない(印には解除予定を残さず、残っていたとしても、もう過ぎているかもしれない)。rollout がまだ無くても聞く——問い合わせは rollout が見つかるのを待たない。「まだ止まっている」と答えれば、上の間隔で聞き直す。
  • 再開の指示を打つのは、解除を確かめた時点で席を占めている起動である。問い合わせの間に席が起動し直されていれば、古い起動は打たず、印も消さない。新しい起動は印から止まった状態で始まり、自分で聞いて打つ——止まった時の起動と同じかどうかで指示を捨てることはない。
  • 印は、再開の指示を席へ打つ前に消す。その間で落ちれば、席は指示を受け取りそこなう。逆の順で落ちれば同じ指示を二度打つことになり、そちらを避けた。

app-server へは、問い合わせのたびに codex app-server を一つ起動し、stdio で聞く。 送るのは一行一つの JSON で、initialize・initialized・account/rateLimits/read の三つだけである。会話(thread)は始めず、モデルは呼ばない。標準入力を閉じればプロセスは終わる(2026-10-05、0.160.0 で実測。全体で 0.5 秒未満)。20 秒で答えが無ければプロセスの木ごと止め、「応答なし」とする。起動の行は席のアカウントの起動コマンドに app-server を付けたもの(.exe 以外は cmd.exe /C 経由)で、席の環境変数・作業ディレクトリ・CODEX_HOME をそのまま渡す——聞かれるのはその席のアカウントである。共有 daemon の AF_UNIX ソケット(~/.codex/app-server-control/app-server-control.sock)でも聞ける(#290 で実測)が、採らない:Rust の標準ライブラリは Windows で AF_UNIX を持たず、ソケットは WebSocket の枠付けを要し、daemon は起動時の環境を全クライアントで共有するため席ごとのアカウントを区別できない。daemon を起動させる副作用も持たない。

読み方とその判断はアプリから切り離して置く。 rollout の行から止まり・ターンの始まり・解除時刻を読むこと(codex::status::Tail)、app-server の答えの読み方、止まり・問い合わせの予定・解除・再開の指示への応答待ちの状態、印のファイル(Mailboxes)、知らせと指示の文面は crates/mcp-config(codex::limit)にあり、テストで確かめる。印の置き場所とトピックと一緒に消すことは crates/topic-index(mailboxes_path・delete)にある。問い合わせ・知らせ・印の読み書き・起動のときの確認(Limiter::open)・再開の指示の打ち込みは src-tauri/src/codex_limit.rs、見張りは五欄と同じ codex_status.rs の一本である。

Codex の席は自分の様子を app-server の知らせで知らせる(#326)

app-server 方式の席では、アプリが席の server のクライアントであり続け、その接続に届く知らせを読む。 新しい接続も設定も足していない。#299 の接続は、席が生きている間ずっと届くものを読んで捨てていた。#326 からは、それを席ごとの Activity(crates/mcp-config/src/codex/activity.rs、tauri を持たずテストされる)へ渡し、画面に出す語か接続の有無が変わったときだけ seat-activity の事象(topic_id・account_id・pty_id・connected・thread_status・word・line・waiting)で画面へ送る(src-tauri/src/codex_app_server.rs)。thread_status(#329)はスレッドが idle か active か、まだ分からない(null)かであり、その変化でも送る。起動・再開の答えの thread.status も、ターンの前の idle の知らせも、受け取ったときにそのまま送る(#368)。何を出すかは「走っているアカウントが何をしているか」。

  • 読む知らせは、対応 CLI の生成スキーマの名前である(Codex CLI 0.160.1 の codex app-server generate-json-schema、通常版と --experimental 版。公式 docs の名前とは違う箇所がある、#324):thread/status/changed、turn/started、turn/completed、item/started、item/completed、thread/closed。thread/start / thread/resume の答えの thread.status も初めの様子として一度読む。
  • 起動・再開中の知らせも捨てない。 thread/start などの答えを待つ間(Started::call)に届いた答え以外の知らせも同じ Activity へ渡し、端末を繋いだ後の読み手(adopt)がそれを引き継ぐ。新しいスレッドの id は thread/start の答えで分かるため、それより前に届いたスレッドの知らせは持っておき(256 件まで)、id が決まってから読む。再開では id が先に分かっている。
  • 席のスレッドの知らせだけを読む。 threadId が違うもの(サブエージェントのスレッドなど)は読まない。
  • server からの要求には答えない。 承認や問いの要求(id を持つもの)は読まず、答えもしない。承認は今どおり端末(TUI)が行う。
  • 接続が切れたことを一つの状態として送る。 事象は語とは別に connected を持つ。読み手は切れた時点で connected: false を一度送って終わり、画面は「様子不明」を出す(待機中の切断でも送る。何を送るかは Reporter が決め、テストされる)。「待機」は送らない。
  • 席の取り違えを防ぐ。 事象は起動の端末の id を持ち、画面は同じ席の次の起動の端末へ前の起動の知らせを当てない。
  • 届いた知らせの種類を記録する(#329)。 席の接続に届いた知らせ(id を持たないもの)ごとに、[codex-activity-probe] <method> room=<トピック id> account=<アカウント id> thread=<status.type か -> at=<UTC 時刻> の一行を %APPDATA%\org.liplus-project.pullcept\logs\codex-activity-probe.logに追記する。書くのは method 名と params.status.type だけで、本文・コマンド・引数・文章は書かない。起動・再開中に届いたものは席が決まるまで持っておき(1024 件まで)、最初に書く。ファイルは接続の間開いたままにし、一行を一度に書く。書けなければ黙って捨てる。知らせの数だけ行が増えるため(文章の差分の知らせも一行になる)、確かめが済めば外す前提の観測である。標準エラーには出さない。
  • hook 方式の Codex の席と Claude Code の席には無い。 前者にはこの接続が無く、rollout からの推測は同じ確かさにならない(#324)。どちらも今の語のままである。

Codex の launch と sidecar の対応(#315)

account と topic は登録先を指すが、CLI 内の子エージェントは同じ値を継承する。Codex の起動では PULLCEPT_LAUNCH_ID と PULLCEPT_ROOM_ADMISSION を launcher が設定し、room MCP の環境へ渡す。sidecar はプロセスごとにランダムな instance ID を生成し、同じプロセスの再接続でも保持する。MCP server を構築する前に、room と同じ bearer 認証で /room/sidecar-admission へ起動 ID・account・topic・instance を申告する。secret は body・argv・ログへ載せない。

launcher の台帳が同じ起動の live な PTY を持つときだけ最初の instance を対応付ける。app-server 経路では PTY より前に MCP を準備するため、起動中は launcher が実際に spawn した app-server プロセスの生存を照合する。この対応を Running の台帳へ引き継ぎ、起動失敗で破棄する。別 instance は拒否し、同 instance だけが再接続できる。登録中の台帳は最大 3 秒、50 ms 間隔で待ち、未知・古い・不一致・終了した起動や通信失敗は空 MCP server(instructions 無し、tools 空)になる。待機中に MCP 初期化は行わない。socket でも対応と liveness を再検証し、account や名前の全体的な重複拒否にはしない。参加者 ID は launcher 側が生成して同 instance へ保持する。起動終了・次の起動で対応を継承しない。

台帳の Starting は起動した CLI の admission 要否を保持し、Codex だけに登録待ちと socket の必須判定を適用する。Claude の起動途中や account を持たない外部接続の既存経路は保つ。合成試験では親・別 instance・再接続・古い起動・登録競合・複数 topic を確認し、実際の子起動による参加者数はマージ後の実機観測に残す。

Claude 親の利用上限と手動再開(#342)

親 ID は fresh の launch_line.session_id と明示 resume の ID から保持する。PULLCEPT_CLAUDE_LAUNCH は Claude 用に起動ごとに生成する nonce で、Codex の sidecar admission 用 PULLCEPT_LAUNCH_ID と別物である。既存の statusLine と activity HTTP hook は nonce を header に渡し、親 ID・topic/account・現在の起動に照合する。Starting 中の検証済み transcript_path はその起動へ保持し、PTY が登録された後に一つの watcher が追う。旧起動の callback は同じ親 ID の resume にも採用しない。

保存範囲は launcher の CLAUDE_CONFIG_DIR(未指定なら ~/.claude)配下の projects/<project>/<親ID>.jsonl に限定する。報告された path を canonical 化して basename・親 ID・保存範囲を検証し、子の subagents フォルダー、任意ファイル、範囲外 symlink を拒否する。CLAUDE_CODE_PROJECT_DIR_NAME による名前変更と長い cwd の派生名にも、reporter の検証済み path で対応する(Claude Code 公式 sessions)。resume の既存ファイルは spawn 前の EOF を採り、未知 path や置換されたファイルは launch 時刻より前のイベントを除く。欠落・部分追記・不正 JSON は回復の証拠にせず、本文を状態・ログへ保存しない。

親の isSidechain=false の新しい assistant にある isApiErrorMessage=true / error=rate_limit の構造化拒否で停止する。回復はその後の requestId と実モデルを持つ正常な親 assistant のみであり、API error フラグの未設定を許容するが <synthetic> の応答では解除しない。reset 時刻や usage 値だけで解除しない。停止と回復はパネルの「制限中」と再開の指示に使い、部屋の投稿は止まっている間もその席の端末へ通常どおり打つ(#401 で預かりを外した。制限中の発言はトピックの記録にあり、read_room_history で読める)。人間は制限解除後に端末から通常どおり手動再開できる。解除予定が分かっていれば、その時刻を過ぎたところで Pullcept が端末へ再開の指示を一度打つ(下記「解除予定時刻に親の再開を試す」)。SubagentStop の到着を捏造せず、保持した subagent 一覧は実 hook のまま扱う。

Claude の席は受け箱(mailbox)を持たない(#401)。止まっていることは親記録そのものから読む:起動前の親記録で最後の親の出来事が答えられていない拒否であれば、その起動は「制限中」から始まり、再開の指示を予定する(下記 #366)。以前の版が書いた *.claude-mailboxes.json はもう読まない。実機の親制限→再開→正常応答の確認は、合成試験の成功だけでは完了としない。

JSONL は一行 4 MiB、読取一巡 1 MiB を上限として一時的に JSON を解析します。本文を保存・ログする用途ではありません。4 MiB 超の行は次の改行まで読み飛ばし、壊れた断片で回復しません。その正常応答が上限を超えた場合は「制限中」のまま、次の読取可能な正常親応答で回復とします。

transcript が未作成・未捕捉でも、現在の nonce・親 ID・PTY に合う reporter の既存メトリクスと活動は維持します。監視対象の採用は独立に検証し、後の reporter で有効な親ファイルができた時に開始します。親 ID が未知の場合は停止・回復を判定しません。再開ファイルの探索が曖昧・読取不能な場合は保存 ID を消しません。

Claude stats 自体にも PTY ID を含め、画面が現在採用した PTY と照合するため、旧起動の遅れた表示イベントは適用しません。保存した制限状態とメトリクスは現在の nonce・親 ID・PTY の生存を照合して一秒ごとに再通知するため、webview の再読込で席を拾い直した場合も backend の制限状態と表示を揃えます。

利用上限の通知主体を移行する(#355)

本体のお知らせは Pullcept(自動通知) が発言し、本文末尾へ (返信不要) を一度だけ付ける。アイコン・履歴は from_app と app origin の構造的判定のままで、同じ名前を名乗る参加者を app 扱いしない。

親の停止通知は現在の JSONL watcher に集約する。拒否の quotaLimits.resetsAt(root または message)にある妥当な epoch から解除予定を読み、未知なら不明とする。StopFailure は rate_limit だけを通知専用 HTTP へ渡し、親 hook 自体からは通知しない。子は認証・現在の launch nonce・親 ID・PTY を照合した後に別対象として通知し、親 gate は触らない。子の reset は共通入力の transcript_path を親 file として検証し、agent_id から期待する子 file を導出する(SubagentStop 固有の agent_transcript_path は必須にしない)。期待親の projects/<project>/<親ID>/subagents/agent-<子ID>.jsonl を canonical 化して末尾最大 4 MiB の新しい構造化 metadata から読む。範囲外・旧履歴・未捕捉の場合は reset を推測しない。通知用の別参加者・WS 接続・AI 応答を作らない。

同 launch 内の対象別停止 episode を重複排除する。親は正常応答による gate 解放後に次の episode とし、子は検証した新しい正常応答の identity で区別する。子の metadata が未捕捉なら同じ子の停止をまとめ、根拠なく次の episode にしない。子の予定時刻が来たときは一回だけ「回復は未確認、端末で再試行できます」と知らせる。親の予定時刻では、この知らせの代わりに再開の指示を打つ(#357、下記)。reset だけでは回復としない。通知は現在の live launch の app 巡回中だけであり、アプリ終了中の OS task による保証は加えない。一般 API エラーと許可待ちの新しい通知は登録しない(公式 StopFailure input)。

新しい対応 launch は PULLCEPT_LIMIT_NOTICE_OWNER=app-v1 を持つ。app の HTTP hooks を実際に launch settings へ登録できる場合だけ marker を設定し、利用者の独自 --settings を持つ経路などへは設定しない。外部 hook は自動変更しない。scripts/notify-stop-migration.ps1 は移行用 wrapper、scripts/install-notify-stop-migration.ps1 は明示実行する installer である。

移行は次の順序で行う。まず対応 app で新しい親/子の Pullcept 通知と解除予定を確認する。続いて既存 hook をレビューし、抑止したい旧予約 task の exact task 名・registration server 名・project の絶対 path の組だけを JSON 配列へ記録する(キーは task / server / project)。別 topic、過去の未移行予約を一括選択しない。予約を引き継いで app が通知する保証はなく、抑止対象は実機で通知所有を確認した停止だけにする。

# 先に -WhatIf で対象を確認する。実行は親/操作者がレビュー後に行う。
.\scripts\install-notify-stop-migration.ps1 -HookPath 'D:\Users\hal\Claude\.claude\hooks-local\notify-stop.ps1' -SuppressedResumeFile .\selected-resume-scopes.json -WhatIf
# 確認した同じ引数から -WhatIf を外すと、backup 検証後に wrapper を配置する。

installer は元 hook を同じ folder の一意な .legacy-*.ps1 に byte 一致で backup し、実行用の .fallback.ps1 を作って旧 scheduler の既知の式だけ元 wrapper path を予約先に向け、移行設定を <hook>.pullcept.json に保存してから wrapper を置く。byte 一致の backup 自体は変更しない。未知の scheduler 式なら元 hook を変えず拒否する。これにより旧 launch の fallback が作る新しい予約も wrapper の exact ledger を通る。settings.json や他の hook は編集しない。新 marker の rate_limit は旧投稿を抑止し、marker のない旧 launch と一般 API エラーは backup の旧処理へ渡す。permission_prompt は通知しない。旧 -Resume は exact task/server/project が移行設定に一致するものだけ旧 WS 通知を抑止する。その他の task は従来 handler に渡す。task 自体を一括削除しない。再適用は拒否して元 backup を保つ。rollback は表示された backup を元 hook path へ戻し、確認後に対応する .pullcept.json を除く。

テンプレート配置だけでは、稼働中の旧 launch と旧 hook の通知は統一されない。新 launch への切替・外部 wrapper の適用後に、別名の自動通知席が増えず親/子・解除予定・返信不要が本体の発言に保たれることを実機で確認する。repo の合成試験は実機確認の代わりにしない。

解除予定時刻に親の再開を試す(#357)

解除予定時刻を過ぎたら、Pullcept がその席の端末へ再開の指示を一度打つ(#357、#401 で簡素化)。 #342 では、予定時刻を過ぎても人が端末に打ち込むまで席は止まったままだった(2026-10-09 22:16 に止まった Lay と Lin は、解除予定 10/10 01:20 を過ぎても朝まで戻らなかった)。回復の判定(正常な親応答)は #342 のままである。対象は親だけで、子エージェントの上限(#355 で親と分けたもの)には打たない。

  • いつ打つか。 親の拒否が quotaLimits.resetsAt で解除予定を名指していれば、その 30 秒後(Codex の問い合わせと同じ GRACE)。解除予定が分からなければ打たない——打つ時刻の根拠が無いので、端末での手動再開を待つ。停止の知らせもそのどちらかを言う。
  • どこへ打つか(#401)。 その時刻に席を占めている端末へ打つ。止まった時の起動と同じ端末かどうかは照合しない。見張りはその席でいま走っている起動の端末についてだけ回り、打つのはその端末である。#399 / #400 では合図を部屋の通知として、本人の起動(topic・account・nonce・PTY)に一致するときだけ届け、一致しなければ捨てていた。制限中に Pullcept を起動し直すと識別子が変わり、合図が届かなかったと見ている(2026-10-12 02:29、Lin・Lay とも合図が来なかった)。
  • 端末がいなければ(#401)。 打たない。「再開待ち」の印は、親記録に残る答えられていない最後の拒否そのものである。その席の端末が次に起動したとき(同じ会話の再開)にそれを読み直して打つ(下の #366)。新しい会話で起動した席には打たない。中断した作業がその会話に無いためである。
  • 何を打つか。 claude_resume::PROMPT(「利用上限の解除予定時刻を過ぎました。中断していた作業を再開してください。中断していた作業がなければ、そのまま待機してください。」)を、部屋の札(発言者 Pullcept(自動通知)、role は app)を付けて一回の打ち込みで打つ。札の message_id は、同時に部屋へ出す「再開の指示を打ちます」の知らせの id である。その知らせ自体は本人の端末へは打たない。許可待ちの席(#346)では待ちが終わるまで預かり、人が端末に打ちかけのときは欄が空くまで待つ。預かった指示は別の起動へ持ち越さない。
  • 正常な親応答が返ったら。 回復であり、それ以上は打たない。部屋への知らせも出さない。手動の再開や部屋の発言で戻ったときも同じである。#389 の「解除を確かめてから再開指示を渡す」入力は、Claude の席では送らない。打つ指示そのものが再開の指示だからである。
  • 制限エラーが返ったら。 指示より後の時刻の親の拒否を読めば、まだ解けていないだけとして、間を空けてもう一度打つ。新しい拒否が名指す解除予定の 30 秒後が今から 1 分より先ならその時刻(5 時間の後に週次で止まった等)、そうでなければ 1 分・2 分・5 分・10 分・15 分と空け、以後は 15 分ごと(Codex の BACKOFF)。打ち直すときの知らせは「まだ利用上限で止まっていたため、もう一度打ちます」である。指示より前に書かれた拒否は答えに数えない。
  • どちらも返らなければ。 指示から 2 分(ANSWER_WAIT)たっても拒否も正常応答も読めなければ、利用上限以外の理由(起動時の選択画面、ログイン切れ、信頼の確認など)で止まっているとみなす。部屋へ「端末を見てください」と一度だけ知らせ、打ち直さない。送信キーが画面の選択肢を選ぶおそれがあるためである。人が端末で手動再開し、その試行が拒否されて解除予定を名指せば、自動の再開がまた始まる。その席の親が後で正常に応答したら(同じ起動でも、起動し直した後でも)、「再開しました。先の『端末を見てください』は解消しています」と一度だけ知らせ、先の知らせを立てたままにしない。知らせたかどうかは席(トピックとアカウント)ごとにアプリのメモリにだけある。
  • 部屋への知らせ(#401)。 再開の側で出すのは、実際に打つときと、上の「利用上限以外の理由」のとき、その「利用上限以外の理由」の席が戻ったときだけである。端末のいない時刻には何も出さない。止まったときの知らせ(#355、「○○ の親セッション は利用上限で停止しました。解除予定は △△ です。」)は今までどおり部屋全体へ出す。止まっている相手に作業を振らないためである。子の予定時刻の「回復は未確認、端末で再試行できます」も今までどおりである。
  • 記録する(#401)。 判断ごとに一行を %APPDATA%\org.liplus-project.pullcept\logs\claude-resume.log に書く:[claude-resume] <action> room=<トピック id> account=<アカウント id> at=<時刻> <詳細>。action は typed(打った)、refused(端末が受け付けなかった)、pending(打たずに印を残した。起動が印を読み直したときと、予定を残したまま端末がいなくなったとき)、still-limited(制限エラー。次に打つ時刻を添える)、recovered(正常応答。指示の後か、ほかの入力の後か。「利用上限以外の理由」を知らせた席が戻った知らせを出したときも一行)、other-cause(応答なし)である。会話の本文は書かない。

予定・回数・文面・記録の行の判断は crates/mcp-config/src/claude_resume.rs と claude_notice.rs(tauri を持たずテストされる)、打ち込み・知らせ・記録の書き込みは src-tauri/src/claude_limit.rs の見張りの一秒ごとの巡回が行う。予定そのものは起動ごとのメモリにだけあり、起動をまたぐ印は親記録である。

止まったまま起動し直した席も、解除予定に打つ(#366)。 起動前の親記録の末尾(読み始めの位置より前、最大 4 MiB)からその親の最後の出来事を読む。それが拒否なら、その起動は「制限中」から始まる(#401 より前は、Claude の受け箱が残っていることも条件だった)。拒否が解除予定を名指していれば予定を組み直す——解除予定がまだ先ならその 30 秒後に一度、もう過ぎていれば起動直後に一度打つ。そのあとは起き直す前と同じである。最後の出来事が正常な親応答なら、拒否は答えられているので組み直さない。解除予定が分からない拒否、親記録が無い起動(新しい会話での起動)、別の親や子の行は何も組み直さない。読むのは起動する席自身の親記録だけで、ほかの席の予定には触れない。その拒否は読み始めの位置より前にあるので、見張りがもう一度読んで二重に打つことはない。2026-10-10、Lin と Lay が 18:27 に止まり(解除予定 20:20)、19:27 に席を起動し直したところ、20:20 を過ぎても合図が出なかった。それを受けた変更である。判断は claude_limit::last_rejection と claude_resume::Resume::restored(どちらも tauri を持たずテストされる)、組み立ては src-tauri/src/claude_limit.rs の configure が行う。

ニックネーム(#383)

Account.nickname は任意の文字列で、作成・編集時に保存する。null・未設定・空白のみは name を呼称として使う。name は元の名前という表示属性であり、nickname が設定されていれば宛先の別名にしない。人間・Claude Code・Codex CLI(PTY と app-server)・汎用 CLI は同じ呼称を使用する。MCP サーバ通知は nickname/name の対応を投稿時に読むが、従来どおり席を取らず宛先候補には出ない。キャラクターや CLI の設定・個人の実値は変更しない。

保存成功後、全 topic の接続席を account id で更新する。接続 id・floor 位置は保ち、稼働中の席の発言・応答待ち解除には更新後の呼称を使う。再接続でも保存済み設定を採用する。切断中の行は設定の呼称を表示する。過去の投稿の呼称・元の名前・宛先は保存時のままで、旧設定や旧ログを一括書換えしない。

Codex の席へスマホから接続する(#419)

Codex CLI アカウントの「起動」で「スマホから接続」を選び、決定してから席を起動する。app-server 方式も有効になる。既定は無効で、保存済みの他のアカウントや稼働中の席の起動方法を変えない。稼働席の ⋯ →「スマホから接続」を開くと、状態と接続の開始・停止が表示される。接続済みになったら「ペアリングを更新」で QR、手入力コード、有効期限を表示し、公式 ChatGPT / Codex アプリで接続する。期限切れはもう一度更新する。ChatGPT 認証とアカウント側の許可が必要で、API キー認証は対象外。CLI の非対応・拒否・時間切れは画面に表示し、端末の利用は続けられる。

QR はアプリ内の qrcode ライブラリで生成し、外部生成サービスへ送らない。https://chatgpt.com/codex/pair?pairing_code=… の公式ペアリング形式を使う。QR とコードは保存しない。画面を閉じる、隠す、有効期限を迎える、席が終了する、接続を止めると表示を消す。画面を閉じるだけでは接続済み端末を失効させず、relay も停止しない。「接続を停止」で今の backend の relay を停止する。次の席の起動ではアカウントの選択に従って接続を試みる。接続希望の変更は ephemeral:true で native 設定に保存しないが、native enrollment は専用 DB へ保存され得る。

接続する席だけは Pullcept のアプリデータ内 codex-seat-sqlite/<SHA-256>(topic/account の UTF-8 バイト長を8byte big-endianで区切り、版付きnamespaceと両IDから導出する完全長64桁hexの単一component)を安定した sqlite_home として使う。分離対象は relay 登録だけでなく SQLite 群全体で、既存の一覧・検索・キャッシュの見え方が変わる場合がある。既存 DB をコピー・削除で移行しない。CODEX_HOME は変えず、認証・rollout 履歴ファイル・writer lock の範囲を保つ。トピックに保存された会話 ID は従来どおり明示して resume する。設定を外した次の起動は元の保存先を使い、専用 DB を削除しない。

制御は席の既存の認証済み loopback backend に短命の専用接続で送り、activity を読む接続は変更しない。要求ごとの接続と ID、最大 30 秒の応答期限、席の起動世代、PTY 生存の前後照合で遅延応答を隔離する。起動時 enable より後に届いた明示操作を優先する。承認要求にはこの接続から答えず、既存の承認所有・権限分割を保つ。remote TUI の行に権限上書き引数を追加しない。

検証範囲: 2026-10-16 の CLI 0.162.1 で、隔離した単一 WS + TUI + Android の同一試験会話入力、および同一 CODEX_HOME / 別 SQLite の二 backend の約16秒の connected 共存を確認した。製品コードの合成試験は起動計画・相関・秘密除外・画面寿命を扱う。製品への実適用、分離後の既存会話 resume / writer lock、複数端末表示と選択 routing、承認、切断再接続、長時間の共存は別途実機確認が必要。connected 表示や CI 成功をこれらの実証とは扱わない。

Windows の専用DB保存先(#426)

#423 の旧形式 id-<topic hex>/id-<account hex> は、標準のアプリデータrootとUUIDの組合せでも保存先が長くなり、Codex CLI 0.162.1の新規履歴DB初期化でSQLite14を起こす条件があった。#426から固定長SHA-256形式を使う。最長DB名 thread_history_1.sqlite と -journal を含む全pathをWindowsのUTF-16単位で数え、248単位を超える場合は実spawn前にアプリデータ保存先の短縮を促す。DB本体だけで判定せず、長いユーザー名や非ASCIIrootにも同じ検証を行う。

旧専用DBは削除・コピー・再利用せず残す。新しい保存先ではrelayの再登録・ペアリングが必要になり得る。CODEX_HOMEと明示resumeの会話IDは変更しない。接続を選んでいない席の保存先・起動は変更しない。

2026-10-16の隔離native0.162.1試験では、developer fixtureをモデルturnなしで永続化したあと、履歴DB本体251字で取得成功、252字で失敗した。slash/extended prefix指定でも252字の失敗は変わらず、同じ252字の新規fixture DBをINCREMENTAL/WALで事前初期化すると成功した。nativeはINCREMENTALをWAL切替より先に設定するため、初期rollback journal(+8字)が失敗する説明と合うが、個別OS syscallは追跡していない。失敗したfixtureを同じ隔離HOME・別の短いSQLiteへcold resumeし、元thread ID一致と履歴取得成功も確認した。修正後の実Rust保存先関数を呼んで得た188字のDB本体pathでも、同じ失敗fixtureのcold resumeと履歴取得が成功した。Pythonで長いDBが開けることやこのfixtureの成功を、製品の元のLuna会話の実証とは扱わない。

Clone this wiki locally