Skip to content

spec(brake): the operational-copy window has no structure that survives its closer disappearing #1901

Description

@smileygames

observation

skills/evolution-parallel-agent-eval step 2 は、rules/** を対象とする draft について operational copy を .claude/ に適用することを必須にしている。step 5 がそれを戻す。この「窓」が開いている間に何が起きるかは、手順のどこにも書かれていない。

2026-09-06、PR #1899(issue #1896)の brake 1 を 3 ラウンド回す過程で、同一ワークスペースの別セッションが .claude/ をライブに複製する測定(#1854)を並走させていた。以下はその実測。

  1. 窓が約 4 時間半、閉じられないまま残った。 ラウンド 2 の評価者が spawn 直後に API 429 で早期終了し、その後 spawn 元セッションでコンテキスト圧縮が挟まった。閉じる手が動くのは spawn 元が次に動ける瞬間であり、それは手順の外にある。予告した窓長は「spawn 直後に閉じる、実測 1 分弱」だった。
  2. 窓の内側で測定の腕 2 本が複製された。 複製時刻は宣言時刻の 36 秒後と 1 分 41 秒後。書き込みとの時間的重なりはなく(引き裂きなし)、対象ファイルが測定対象の発火判定に効く経路も無かったため、測定自体は有効だった。
  3. 窓の内側でセッション境界を跨いだ主体が、窓が閉じたあとも draft を保持した。 別セッション側と、窓を開けた当セッション自身の双方で発生。ディスクの復元では回収されない。双方とも復元後の canonical 再読で上書きした。
  4. 窓の内側で何が読まれたかを事後に確定する手段が、どちらの側にも無かった。 測定ハーネスは腕へ複製した rules 本文の digest を残していない。brake 1 側も、閉じたときのハッシュしか記録していない。今回それを言えたのは ~/.claude/projects/ の副産物が偶然残っていたためで、設計されたものではない。
  5. 「印が無い」の多義性による誤読が 1 件。 測定ハーネスのロック(scripts/measure_rule_effect.py:227 acquire_lock)は正常終了時に消えるため、「一度も走っていない」と「走り終えた」が同じ見え方になる。この区別を確かめずに「腕は複製されていない」と報告し、後から訂正した。

分析

危険の回避は口頭の相互通知で行った。2 セッションが部屋で宣言し、互いにハッシュを当て、materialize を保留した。それは機能したが、片方が落ちた瞬間に無効になった(1 がそれ)。

rules/model/subtractive-structural-beauty.md Spec write applies (B) の rider は、実行が保証されない手順を構造で置き換えることを要求している。窓の開閉は現在この形になっている ―― 開けた主体が閉じる手順であり、その主体が落ちれば実行されない。

当初は「窓の最小化」(step 5 を「そのラウンドの全 spawn 完了直後」へ前倒し)を本体と見ていたが、1 の実測でそれが誤りだと分かった。窓の長さを決めているのは手順ではなく、書いた手が次に動ける瞬間である。最小化しても、閉じ手が消えれば開きっぱなしになる。

収束

ディスク上の印(マーカー)を置く。 これが本体。3 つの穴が 1 つの構造で塞がる:

  • 閉じ手が消えた → 印が残ること自体が「開いたまま誰も閉じていない」の信号になる。次にワークスペースに入った主体が読める(落ちたセッションの復帰を待たない)
  • 窓の中で読まれた本文が言えない → 印に開いた時刻と digest を書く。両端が残る
  • 部屋の外の読み手 → 印はディスクにあるため、この部屋にいない主体にも届く

印の要件:

  1. 作成が二値であること。 mkdir の成否のように、読んで判断して書く隙間が無い形。scripts/measure_rule_effect.py:227 acquire_lock が既にこの形。
  2. 中身 = 開いた時刻、適用した版の digest、復帰先 digest、バックアップの位置、開けた手順の識別子。 復帰先とバックアップ位置が要るのは、印を見つけた別の主体が閉じられるようにするため。今回は口頭で共有したから相手が当てられたが、次はそうとは限らない。
  3. stale 閾値を手順ごとに引けること。 既存の STALE_LOCK_SECONDS = 6 * 60 * 60scripts/measure_rule_effect.py:43)は測定には妥当だが brake 1 の窓には長すぎる。今回の 4.5 時間でさえ閾値の内側で、奪取は一度も発動しない。
  4. 閉じるときに消すのではなく、閉じたことを書いて残すこと。 5 の誤読の直接の対策。消す設計だと「印が無い」が「一度も開いていない」と「閉じ終わった」の両方を意味する。
  5. 置き場所は .claude/ 側。 brake 1 の窓と測定ハーネスの両方から見える必要がある。既存ロックは測定ハーネス同士の排他であり、brake 1 の窓を知らない。

印でも解けないもの(記録として):

  • 意図しない読み手の回収。 印は新規の読み手を止められるが、既に読んだ主体の context は戻らない。3 がそれ。印にできるのは「窓の内側でセッション境界を跨いだ主体は再読せよ」を検出可能にすることまでで、再読そのものはその主体の仕事になる。
  • 印を置く手も落ちうる。 ただし落ちれば印は残るため安全側に倒れる。lock_age_seconds の docstring(scripts/measure_rule_effect.py:207-215)が、印を取ってから中身を書くまでの隙間で落ちた場合まで扱っている。

対象外

  • 窓と測定の相互排他そのもの(印を置いたうえでなお残る重なりの話であり、この issue の主題ではない)
  • skills/evolution-parallel-agent-eval step 5 の位置を動かすこと(1 の実測により、これは本体ではないと判断した)
  • 測定ハーネス側のロック実装の変更(共有化の設計が決まってから)

target files

  • skills/evolution-parallel-agent-eval/SKILL.md — step 2 / step 5 に印の取得・解放を組み込む
  • scripts/ — 印の実装(measure_rule_effect.py のロックを切り出して共有するか、別に置くかは設計時に決める)
  • skills/evolution-rule-effect-measurement/SKILL.md — ライブ木を複製する側が同じ印を見るようにする
  • docs/ — 対応する日本語版(同一 PR)

位置づけ — 「実行主体が居ない」の族の中で

同日、同族の欠けが三つ出た。いずれも「規定は書かれているが、それを実行する主体が居ない」形。ただし居ない理由が違い、修理も別々になる

issue 規定 主体が居ない理由 修理の向き
#1894 期限(tally の閾値到達) 最初から割り当てられていない 担当と発火モーメントを置く
#1750 観測窓 読む面のほうが先に消える/存在しない 面を寿命の長いものにする
本 issue 窓を閉じる手順 主体が途中で落ちる 落ちても続きを実行できる構造にする

より効く分け方は「なぜ居ないか」ではなく、居ないことがどう見えるか

本 issue の印の要件 4(閉じるときに消すのではなく、閉じたことを書いて残す)は、この分類でいう「成功に見える形」への直接の対策である。消す設計だと「印が無い」が「一度も開いていない」と「閉じ終わった」の両方を意味し、後者を前者と読んだ誤報告が実際に 1 件出ている(observation 5)。印を残す設計にすれば、本 issue の欠けは痕跡が残る形に留まる。

この分類は本 issue の実装対象を増やさない。要件 4 が既に対応しており、記録として置く。

追加観測(2026-09-07、PR #1904 の brake 1 で窓を 4 回運用)

同一ワークスペースの 2 セッション(窓を開ける側と、開けない側)で 4 回の窓を運用した実測。窓長は 53 / 33 / 33 / 35 秒。#1899 の 4.5 時間に対して、閉じ手が生きている限りは短く閉じられることが確認できた。4 回目は 3 回目の evaluator が spend limit で落ちたあとの同一ラウンド再 spawn であり、閉じ手が落ちた状態を挟んでも窓は正しく閉じられていた

以下は上記の要件 1〜5 では埋まらなかった面。要件 6・7 として追加する。

要件 6 — 弁別文字列を、分ける二版の名指しと両側の出現数つきで開示する

本 issue は既に「印でも解けないもの」として次を記録している ―― 印は新規の読み手を止められるが、既に読んだ主体の context は戻らない。その主体が自分の頭がどちらの版かを言う手段が無い、という面がここに残っていた。

context に載っている本文はハッシュが取れない。ディスクと repo main は取れるが、頭は取れない。測れるのは 2 点だけで、頭は N 個ある(窓を開ける側、開けない側、そして spawn 済みの作業役。作業役の分は spawn 時点のスナップショットであり、resume で再注入されるかは仕様に無い ―― #1902 に未確定として記録済み)。

迂回として実測した形: draft 側にのみ在る一行を、開ける側が開示する。 読む側はその文字列が自分の context に在るかを見て、頭がどちらの版かを申告ではなく照合で言える。4 回の窓すべてで機能した。

ただし成立には二つの条件が要る。どちらも実測で外しかけた。

  • 両側で数えること。 片側に在ることを見ただけでは弁別ではない。canonical 側で 0 であることまで確かめて初めて「分ける」と言える。
  • 分ける二版を名指すこと。 弁別文字列が答えるのは「頭がどの版か」ではなく「頭が、名指した参照より古いか新しいか」。参照を書かないと答えが決まらない。実際に、同じ頭に対して repo main を参照に取った側は「古い」を、ディスクを参照に取った側は「一致」を得た ―― 同じ頭、違う答え、どちらも正しい、という状態が発生した。

限界(記録): 弁別できるのは選んだ一行の周りだけで、「一致」ではなく「選んだ一行の周りでは一致」しか言えない。頭が絡む辺の判定は必ず一段弱い。

要件 7 — 開示 → 待ち → 適用の順にし、待ちの終わりを応答またはタイムアウトで定める

窓 1・2 回目は「開示 → 適用」の順で実行していたが、間隔が数十秒しかなく、予告として機能していなかった。受け取った側が確かめて判断する時間が無ければ、順序が正しくても事後通知と同じになる。実際、読む側はこの 2 回とも「既に適用された状態」しか観測できず、順序について何も言えなかった。

3 回目から開示のあとに待ちを挟む形に変えたところ、読む側が適用前の実体を自分の手で押さえた。ディスクが復帰先のままであること、弁別文字列が canonical 側で 0 であること、mtime が前回の閉じ時刻から動いていないこと。予告が予告として機能した最初の例。

待ちの終わりは 相手の応答またはタイムアウトのいずれか早いほう。応答だけを条件にすると、相手が黙っている場合・席を外している場合に窓が開けられなくなる ―― それは可用性の問題であって安全側ですらない。

開示の位置づけ(要件 6・7 に共通する理由)

開示は相手への礼儀ではなく、開けた手が黙ったときの判定材料。 4 回目の窓の直前、開ける側が spend limit で落ち、閉じた報告が部屋に出ないまま切れた。読む側から見ると「報告が無いだけ」と「開きっぱなし」は報告からは区別が付かない ―― 報告の不在は失敗の証拠でも成功の証拠でもない。このとき読む側は、先に開示されていたハッシュと弁別文字列を使って実体から閉状態を判定した(正しく閉じられていた)。

これは要件 4(閉じるときに消すのではなく、閉じたことを書いて残す)と同じ向きの手当てを、印の外側=開示の側で行う形にあたる。印が置かれれば読む側はそれを読めばよいが、印が置かれるまでの間も、開示があれば判定できる。

対象外(追加分)

  • workspace .claude/ と repo main の恒常差(Li+ adapter update 見送りによる tag 同期の遅れ)。窓の開閉とは別の機構であり、窓が閉じても解消しない。同じ弁別文字列の手で当たることは確認したが、本 issue の主題ではない。別 issue として扱う。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    forming本文を再構築しながら要求を整えている状態specLi+の挙動に影響する仕様・ポリシー・定義

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions