8つのClaude Codeセッション、1つのチェックアウト
要点
- 私はClaude Codeのセッションを中央値で8つ同時に動かしており、1つのチェックアウトで最大10まで走る。30日間で118が他のセッションを一覧し、7がメッセージを送った。
- セッションが仲間について知る必要のあったものは、すべてすでにディスク上にあった。稼働中セッションのレジストリ、入力されたすべてのプロンプト、各セッションが書き込んだすべてのファイル。
- 3つのフックがそれを読む。セッション開始時に誰がいて直前のセッションが何をしたかを表示し、各プロンプトで仲間がメモリに書いた内容を表示し、仲間がチェックアウトを共有している間はインデックス全体を対象とするgitを拒否する。
- 編集を宣言するルールが読み込まれた10分後、あるセッションが別のセッションへ、これから触る場所を促されもせずに伝えた。メッセージ数は10月末に再測定する。
私はClaude Codeのセッションを数多く動かしている。9月21日までの30日分のトランスクリプト: 297セッション、その日のある時点で生きているのは中央値8つ、ピークは12、同一チェックアウト内に最大10。半分は5時間より長く生きる。10に1つは39時間より長く生きる。
連携に必要な機能はすべて備わっていた。ListAgentsはマシン上の他の稼働中セッションを一覧し、SendMessageは名前でそのどれにでも届く。この30日間で118セッションがListAgentsを呼び、7つがメッセージを送った。欠けていたのは話す理由と行動の根拠となる事実であり、どちらもすでにディスク上にあった。
問題: 1つのチェックアウト、8セッション、7メッセージ
最初の2つの帯はセッションが共有するもの。3つ目は全員が書き込み、これまで何も読まなかったもの。
1つのチェックアウト内のセッションは2つのものを共有する。gitインデックスとワーキングツリー: ツリー全体に及ぶ操作(git add -A、commit -a、パス指定なしのstash)は全員の変更に作用し、gitはどのファイルがどのセッションのものか知らない。そしてリポジトリ単位でキーされる自動メモリディレクトリは、セッション開始時に一度だけ読み込まれる。08:00から開いているセッションは、正午に仲間が書いた訂正を決して見ない。それ以外はすべてプライベートだ。外から見れば、仲間はlife-os-a9のような名前とステータス語にすぎない。
3つの失敗が続き、それぞれを測定した:
- 吸収された作業。 9月1日、あるセッションが6ファイルをステージした。数秒後に2つ目のセッションが広範な
git addを実行し、それらを自分のコミットに巻き込んだ。1つ目のセッションに現れた唯一の症状は、git commitが「no changes added to commit」と表示したことだった。 - 再学習されたルール。 mistboardのメモリは、すでにlife-osのメモリにあった3つのルールを、別の言葉で再学習していた。
- 手探りの開始。 その月に302セッションが終了し、引き継ぎを書いたのは2つ。新しいセッションはすべて何もない状態から始まった。
着想: プロセスがすでに書いているものを読む
Claude Codeは、誰も読んでいなかった3つのファイルを保持している。~/.claude/sessions/<pid>.jsonには稼働中の全セッションのid、作業ディレクトリ、名前、ステータス、開始時刻が入る。~/.claude/history.jsonlには入力された全プロンプトがセッションidとともに入る。各セッションのトランスクリプトには全ツール呼び出しが入り、書き込んだ全ファイルのパスも含まれる。それは仲間が知りたいことのすべてであり、プロセス自身が維持している。
そこで各パーツのルールはこうなる。ディスク上にあるものを読み、セッションが最新に保たなければならないフィールドを決して追加しない。手書きの引き継ぎは書き込み率1%だった。ステータスフィールドでもそれ以上にはならない。トランスクリプトの最初のプロンプトは、セッションが書き忘れようのない目的の表明だ。
作る過程でさらに3つのルールが出てきた:
- 各パーツを、その事実が新鮮な瞬間に取り付ける。セッション開始時、各プロンプト、あるいはコマンド実行直前。
- ロックせず拒否する。仲間と代替手段を名指しするガードは、誰にも見えないロックを保持するものに勝る。
- 指示は判断が下される場所に置く。セッションが決して読み込まないファイルにあるルールは、既定で破られる。
解決策: 3つのフック上の4つのパーツ
各パーツは、その事実が新鮮であるフックに乗り、最初の図のファイルを読む。
配線はすべて~/.claude/settings.jsonの1ブロックだ。フックの標準出力はセッションのコンテキストに入り、終了コード2は標準エラーを理由としてツール呼び出しを拒否する。スクリプト、そのテスト、測定はclaude-code-peersにある。以下の抜粋は要点となる部分だ。
{
"hooks": {
"SessionStart": [{
"matcher": "startup|resume",
"hooks": [{ "type": "command", "timeout": 10,
"command": "python3 scripts/peers.py --hook" }]
}],
"UserPromptSubmit": [{
"hooks": [{ "type": "command", "timeout": 5,
"command": "python3 scripts/memory_delta.py" }]
}],
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "timeout": 5,
"command": "python3 scripts/git_index_guard.py" }]
}]
}
}
セッション開始: 直前に誰がいたか、今誰がいるか。 peers.py --hookは、このチェックアウトで最後に終了したセッション(いつ終わり、どれだけ走り、何に取り組み、何を編集したか)を表示し、続いて稼働中の仲間を表示する。その最初のブロックが、誰も書かない引き継ぎだ。手書きのHANDOFF.mdは存在すれば参照され、記述すべきセッションより古い場合は印が付く。意図はプロンプト履歴から、了解の返答を除いて取る。1語から4語のプロンプトはその月の35%だったので、下限は5語。仲間については最初の実質的なプロンプト、最新の1つ前、最新のものを表示する。意図は漂流するし、直近の2つがその漂流を示すからだ。編集中のファイルはトランスクリプトから取り、最新のWriteとEdit呼び出しを求めて逆順に読む。
- mistboard-08 busy · up 36h · 161 prompts · last 1m ago
opened: "can you investigate this game: https://mistboard…"
before: "this widget is still the old style, we gotta use…"
now: "let's also frame it as they won it, since the…"
editing: apps/web/src/jungle-replay-board.ts (1m ago)
各プロンプト: あなたの開始以降に仲間がメモリへ書いた内容。 memory_delta.pyはメモリディレクトリをstatし、このセッションの開始より後に更新されたファイルを、それぞれ一度ずつ、書き込んだ側の名前とともに一覧する。自分の書き込みは、そのパスへの書き込みがトランスクリプトにあるかを確認して除外する。約40 msで、ほとんどのプロンプトでは何も出さない。
threshold = max(session_start, last_run) # last_run: a stamp file
for f in memory_dir.glob("*.md"):
if f.name == "MEMORY.md" or f.stat().st_mtime <= threshold:
continue
desc, origin = frontmatter(f) # description:, originSessionId:
if origin == session_id or written_here(f, transcript):
continue # this session's own write
print(f"- {f.name} ({when(f)}, {peer_name(origin)}): {desc}")
各シェルコマンド: 仲間がチェックアウトを共有している間はインデックス全体を対象とするgitを禁止。 git_index_guard.pyはコマンドを解析してgit add -A、add .、commit -a、パス指定なしのstashを検出し、コマンドが触るチェックアウトを解決し(cd X &&とgit -C Xも含む)、同じgitトップレベル上の他の稼働中セッションをレジストリから読む。ワークツリーは独自のインデックスを持つので決して数えない。チェックアウトに一人なら、コマンドはそのまま実行される。
def peers_in(top, own_session_id):
"""Other live sessions whose checkout is this git toplevel."""
return [j["name"] for j in live_sessions()
if j["sessionId"] != own_session_id
and toplevel(j["cwd"]) == top] # a worktree has its own
peers = peers_in(toplevel(git_cwd), payload["session_id"])
if peers:
sys.stderr.write(
f"Refused: `{what}` while {len(peers)} other live session(s) "
f"share this checkout ({', '.join(peers)}). The git index is "
"shared; name the paths this session changed instead.\n")
sys.exit(2)
全セッション、全リポジトリ: 何度も再学習されていたルール。 ~/.claude/rules/内の6つのmarkdownファイル(バージョン管理されたディレクトリへのシンボリックリンク)が、マシン上の全セッションに読み込まれる。共有ツリーでの振る舞い、トークン予算、どのアカウントがどこに投稿できるか、誰もやっていないと主張する前に何を確認するか。それぞれが根拠を保持するメモリファイルを名指しする。セッション開始あたり約2000トークン、キャッシュ済み。
各パーツには、Claude Codeと同じように標準入力経由で駆動するフィクスチャテストがあり、さらに期待される各フックが登録され、存在し、コンパイルでき、テストを通過することを検証する配線チェックがある。
これまでの結果
配線は夕方に入った。翌朝、仲間が2つ存在する状態でのlife-os内のgit add -Aは拒否され、両方が名指しされた。新しいセッションの「何が起きている?」への最初の回答は、3つの仲間とそれぞれの取り組みで始まり、続いて最後に終了したセッションが示された。貼り付けたものは何もない。そしてメモリフックの稼働中セッションでの最初の1行は間違っていた。仲間が数週間前に作成し私がたった今編集したファイルが、仲間のものとして現れたのだ。originSessionIdは作成者を指し、最後の編集者ではないからだ。今ではセッションが作成していないすべてのファイルについてトランスクリプトの確認が走る。
重要だったメッセージは、全セッションが読み込むCLAUDE.mdに1段落を入れた10分後に来た。同じチェックアウトの仲間のプロンプトが名指しするファイルを編集する前、あるいは仲間が作業中にコミットする前には、それに伝えること。これが00:08に入った。このブログのリポジトリのセッションが00:18に始まった。00:25ごろ、それは促されもせずlife-osのセッションへ次を送った(ファイル一覧は省略):
brianhliou.comのセッションからの連絡: life-osでリネームの後片付けをしています: scripts/lesson_harvester.py、data/lessons.jsonl(重複173レコードの重複除去)、CLAUDE.md:151、briefを2つ、CLI経由でnoteを2つ、その後 lesson_compile.py –apply。名前を指定してステージします。あなたのファイルには触りません。
受信側セッションの表示: 届いたままのメッセージと、送った返信。
返信は1行だった。重複なし、進めてよい、そしてそれが知り得なかった1つの事実、lesson_compile.pyが1時間前にmainで変わっていたこと。作業は7ファイルに触る1コミットに収まり、そのどれも受信側のものではなかった。メッセージは合計3通。このやり取りに新しい機能は何もない。その10分前に変わったのは、判断が下される場所にルールが読み込まれ、セッションが仲間の編集対象を見られるようになり、広範なaddはいずれにせよ拒否されるようになったことだ。
変える度合いで並べると、まず引き継ぎ。これまで全セッションが手探りで始まっていたから。次にガード。損害を記述するのではなく防ぐ唯一のパーツだから。最後にメッセージ。目に見えるのはこれだから。
次は: 1か月後の数字
限界は分かっている。ガードは拒否するがロックはしないので、仲間のgit reset --hardは今もあなたの未コミットの編集を巻き戻すし、それについてのルールはテキストにすぎない。メモリフックが対象とするのはメモリディレクトリでCLAUDE.mdではないため、セッション途中で編集されたルールは、コンパクションが起きたときにしか稼働中セッションへ届かない。レジストリとトランスクリプトはローカルなので、クラウドのセッションはそのすべてから見えない。
これのどれも本来私が作るべきものではなかった。レジストリ、トランスクリプト、履歴ファイル、フック、メッセージングはすべてClaude Codeに同梱されている。同梱されていないのはそれらを結ぶ既定値で、だから1台のマシン上の8セッションは、誰かが測定して配線するまで8人の他人のように振る舞う。その既定値は製品側に来ると思う。aideは同じトランスクリプトの上に自分でSQLiteを載せたものとして始まり、製品は後に同じログの上に/insightsを育てた。それまでの間、配線は設定1ブロックと3つのスクリプトであり、すでに手元にあるトランスクリプトが出発点だ。
検証中の主張は、意図を見せればセッションは話すというものだ。基準値は30日間でメッセージを送ったセッションが7つ。10月末に再測定し、その数字とともにこの記事を見直す。動いていなければ、名簿は制約ではなかったということで、次に見るべきは何が制約なのかだ。