Skip to content

herdr substrate: session 同一性を workspace label アンカーへ移行(#556 の herdr 5 本赤の解消・完成済み実装の救出 land) - #587

Open
proboscis wants to merge 5 commits into
mainfrom
feat/impl-substrate-herdr-session-identity-anchor-r2-607f0c
Open

herdr substrate: session 同一性を workspace label アンカーへ移行(#556 の herdr 5 本赤の解消・完成済み実装の救出 land)#587
proboscis wants to merge 5 commits into
mainfrom
feat/impl-substrate-herdr-session-identity-anchor-r2-607f0c

Conversation

@proboscis

@proboscis proboscis commented Aug 11, 2026

Copy link
Copy Markdown
Owner

何を直すか

herdr substrate の session 同一性の根拠を、実 agent 起動後に herdr 側の実 agent 検出が上書きしてしまう agent 名札から、上書きされない workspace label アンカーへ移行する。これにより未改変 main で赤の herdr substrate 正典テスト 5 本(#556 の症状 2)が green になる。

Refs #556(正典 hy-gate conformance が未改変 main で赤 — 本 PR は herdr substrate protocol 不一致側の 5 本を解消する)

選定根拠(なぜ workspace label をアンカーにするか)

  • protocol 17(herdr 0.7.5)で agent.start が改形され、名前付き shell pane 生成には使えなくなった。代替の名前登録は pane.report_agentagent.rename 経由だが、実 agent(claude 等)が pane 内で起動すると約 2 秒以内に herdr の実 agent 検出が名札を上書きし、agent.get {target: 旧名}agent_not_found になる(2026-08-01 probe n=3 決定的 — conformance/herdr-physics.md 追補に実測記録を同梱)。
  • つまり名札(agent name)は消される対象であり、session 同一性のアンカーには使えない。workspace label は herdr 側の検出に上書きされず、workspace 消滅(唯一 pane の close)と同時に消える = session lifecycle と一致する。
  • 名札依存への回帰は semgrep guard(tests/semgrep_rules/herdr_agent_name_identity_forbidden.hy)で機械的に禁止し、名札消失後も同一性が保たれることを deftest(test-herdr-identity-survives-agent-name-loss)で pin する。

受入条件との突合(発注 4 項)

  1. 正典ファイル全緑(skip/xfail 退避禁止): packages/doeff-agents/tests/test_sessionhost_substrate_herdr.py = 13 passed(main 0698c74 は 5 failed / 6 passed)。独立検証 3 回(2026-08-10 ×2・2026-08-11 正規形 ×1)。
  2. 名札に依存しない同一性の回帰テスト追加: test-herdr-identity-survives-agent-name-loss + semgrep guard 同梱。
  3. tmux backend の挙動を変えない: 差分 9 files に tmux 面のファイルなし(substrate_herdr / deftests / conformance md / semgrep / adopt 3 行のみ)。
  4. PR 本文に選定根拠と Canonical hy-gate conformance is red on unmodified main in the local env (S11b client hang; herdr substrate protocol mismatch) #556 参照: 本欄。

退行確認

packages/doeff-agents/tests 全量 = 4 failed / 1768 passed / 21 skipped — 赤 4 本は main でも同一の環境依存 e2e(実 agentd / 実 MCP を要求)につき退行なし。

出自(provenance)

本枝は ACP 実装席(4 世代目)が 2026-08-09 に完成させたもの。席は保存の 4 分後に稼働中の誤打ち切りで強制終了され「実装消滅」扱いになっていたが、成果は共有 checkout の ref 空間に残存していた。下調べ席の検証と operator 裁定(2026-08-11)を経て、本 PR はその完成品を無改変で救出 land するもの(コミットは実装席の著作のまま・tip 3c22539)。


改訂 2(2026-08-14): 名前解決からも創出順の推測を撤去

差し戻しの指摘(review)

初版の重複 gate は「workspace_id の shortlex 順 = 創出順」を前提にしており、herdr のカウンタが英字から数字へ繰り上がる境界(先行 w35Z / 後発 w350)で勝者判定が反転する。重複 create が自分の gate を通過して生きた session を孤児化し、続く kill が誤った workspace を閉じて生きた方を漏らした。review は (1) 両ファイル(substrate_herdr.hy と conformance/harness.py)への適用、(2) 繰り上がり境界の pin を要求した。

fix_class: root-cause

根因 = herdr の workspace_id という他システムの不透明な識別子から創出順を自前で導き、その順序に session 同一性を託していたこと。id の字面に創出順は無い(下記の実測)。したがって「正しい順序鍵を作り直す」修理は同じ誤りの再演にしかならず、順序を必要としない形に変えるのが根治になる。

実測(稼働 herdr 0.7.5 / protocol 17):

連続 create の実測列 出典
w3NZ → w3N0 / w3MZ → w3M0 前段 27ba33c(全量走行の RPC 記録・200 回中 1 回)
w3ZW → w3ZX → w3ZY → w3ZZ → …繰り上がり… → w303 → w304 → w305 → w306 本改訂 2026-08-14
w35Y → w35Z → w350 → w351 review probe 2026-08-13

'0'(48)は英字(65-90)より小さいので、後から作った id が ASCII でも shortlex でも先行より小さくなる

2 段の改訂(この枝の 2 コミット)

  1. 27ba33c(前段) — 重複 gate を 2 段化(事前照会で既存 session を検出 + 事後照合は同時作成どうしの合意のみ)。shortlex 鍵は「合意のための全順序」に用途限定。vanished 判定で IndexError も解消。

  2. f851c3e(本コミット)名前解決の側に残っていた同じ前提を撤去。herdr-workspace-id-io が保持者一覧の先頭を「その名前の session」として選んでいたため、保持者が 2 つ並ぶ状態(同時作成の敗者が閉じる前・敗者の close 失敗・doeff 外の同名 label・conformance の帯域外 create)で被害が残っていた。

    前段(27ba33c)に対する実測反例:

    holders            ['w42F','w42G']
    帰属観測(R4)      ['w42F:p1']     ← 第 2 holder の pane が不可視
    kill 後の holders  ['w42G']        ← 生きた workspace の漏れ(呼び手には「殺した」)
    

    帰属観測の穴は policy.hy の R4 検証(pane が session に属さない → vanished 即時終端)を通って生きた席の誤終端になる。修理後は:

    • 生死 = 保持者が非空か
    • 帰属観測 = 全保持者の pane 和(tmux list-panes -s -t NAME parity)
    • kill = 全保持者を閉じる(その名前の session が消える parity。消滅済みの workspace_not_found のみ飲み込む)
    • herdr-workspace-id-io(「その 1 つ」を選ぶ面)は撤去
    • conformance/harness.py も同形へ(反証済みの _herdr_workspace_order_key を撤去 — review 指摘 (1) の対)

検証(1:1 対応)

review / 受入の項 検証したテスト・実射 結果
創出順を id から導かない(substrate) packages/doeff-agents/tests/sessionhost_substrate_herdr_deftests.hy::test-herdr-new-session-verdict-ignores-id-order(前段)+ ::test-herdr-label-holders-are-a-set-in-listing-order pass
繰り上がり境界の pin(w35Z vs w350) ::test-herdr-label-holders-are-a-set-in-listing-order(shortlex も素の ASCII も先行/後発を入れ替えることを反例として assert) pass
同じ修理を harness.py にも適用 packages/doeff-agents/conformance/harness.py(_herdr_workspace_order_key 撤去・集合型)+ 実 herdr 実射(holder 2 つ → liveness True → kill → holder 0 → liveness False) pass
孤児化・漏れの回帰(review の END-TO-END repro 相当) ::test-herdr-name-resolution-covers-every-label-holder(実 herdr。前段の実装では red = 上記反例) pass
重複拒否が先行 session を守る ::test-herdr-duplicate-session-rejected(強化: 残存する 1 つが先行の workspace であること・名前解決が先行 pane を指し続けること) pass
正典ファイル全緑(skip/xfail 退避なし) packages/doeff-agents/tests/test_sessionhost_substrate_herdr.py 16 passed(実 herdr server)
恒久ガード semgrep doeff-agents-herdr-label-holders-must-not-be-indexed + fixture + tests/semgrep/test_vm_failfast_semgrep_rules.py::test_herdr_label_holders_indexed_rule_detects_arbitrary_holder_pick(単一ファイル走査・ディレクトリ走査の両モードで実射確認)+ 台帳 249 → 250(tests/test_enforcement_ledger.py) pass
lint make lint-semgrep = 199 rules / 903 files / 0 findingsmake lint-pyright = 0 errors pass

検証の逸脱(declared)

  • conformance 全量走行(DOEFF_SESSIONHOST_BACKEND=herdr)は本改訂では回していない。理由は実行機の負荷(load average 48)で、この suite は静穏機を前提とする既知の環境依存(Canonical hy-gate conformance is red on unmodified main in the local env (S11b client hang; herdr substrate protocol mismatch) #556)。代わりに harness の変更点を実 herdr へ直接実射して確認した(同 label の holder 2 つ → session_exists_out_of_band True → kill_session_out_of_band → holder 0 → False)。harness 側の消費は元から順序に依存しない(kill = 全保持者掃き取り / liveness = 非空判定)。
  • make lint-ruff は main 由来の既存 red 2 件(packages/doeff-agents/tests/test_provider_failure_markers.py の I001 / PT006。origin/main の同ファイルでも同一に再現)。本 PR の差分外につき手を付けていない。
  • make lint-doeff は doeff-linter 未インストールの環境警告(実行機側の欠落)。

並行席との統合

同じ review 差し戻しに対して別席が 27ba33c を先に push していた。本コミットはそれを上書きせず土台として採用し(2 段 gate と用途限定した shortlex 鍵はそのまま活かす)、残っていた名前解決側の前提と harness を上に重ねた。

@proboscis

Copy link
Copy Markdown
Owner Author

実レビュー + 現 main への追随(doeff 保守者側の席から)

PR #569 の実レビュー(→ #569 の comment)の帰結として、本 PR を実レビューし、現 main へ追随させて検証しました。#569 は close 済みです。

追随(merge c98492ac)

本 PR は CONFLICTING でした。現 main を merge し、衝突 2 件を解決しています。いずれも追記型です:

  • .semgrep.yaml — 本枝の doeff-agents-herdr-session-identity-not-agent-name と main の doeff-agents-provider-failure-verbatim-forbidden が同じ位置に来ていたので、両方を残しました
  • docs/adr/enforcement-ledger.json — main の実数(defadr_files 23 / adr_deftest_enforcements 35 / adr_defsemgrep_enforcements 46 / adr_laws 81)へ追随し、本枝が足す 1 ルールぶん semgrep_rules を 248 → 249 にしました。tests/test_enforcement_ledger.py で実数一致を確認済みです。

実装ファイル(substrate_herdr.hy ほか)は衝突していません。

選定根拠を独立に再実測しました(2026-08-14)

稼働中の herdr(ping = version 0.7.5 / protocol 17)に対し、#569 の登録経路をそのまま打った結果:

pane.report_agent + agent.rename + pane.clear_agent_authority -> 名前で agent.get できる
pane.send_text "codex\n" の約 2 秒後 -> agent.get: agent_not_found
                                        pane.list:  {"agent": "codex"}    ← 名札が置換された
                                        workspace.list: {"label": …}      ← label は無傷

本 PR の前提(名札は消える / label は残る)は再現しました。通算 n=4 です。あわせて 同 label の workspace.create は 2 つ目も成功する(w209 / w20A)ことも再実測し、doeff 側が重複拒否を持つ必要があるという本 PR の判断も裏取りしました。加えて workspace_id が shortlex で創出順に単調であること(w346 … w34H を 12 連続 create で確認)と、workspace.list が create を即時反映することも確認しています。

semgrep guard も実際に効きます: 本 PR のルールを #569 のコードへ当てると substrate_herdr.hy:233(herdr-call socket-path "agent.get" …) に的中します。

検証結果

対象 現 main 本 PR + 現 main(c98492ac)
packages/doeff-agents/tests/test_sessionhost_substrate_herdr.py(単独走行) 5 failed / 6 passed 13 passed(3 回とも)
tests/test_enforcement_ledger.py + tests/semgrep/test_vm_failfast_semgrep_rules.py 18 passed
packages/doeff-agents/tests 全量 -m 'not e2e' 7 failed / 1815 passed / 21 skipped

全量の赤 7 本のうち 6 本は test_sessionhost_resume_cross_binding.py で、現 main のコードでも同じ 6 本が赤です(同一 worktree で main の packages/doeff-agents を checkout して実測 = 6 failed / 8 passed)。本 PR による退行ではありません。実 claude / codex の REPL ready gate が 120s で落ちるもので、実測時の機械の load average は 76〜130 でした。

⚠ 1 点、報告しておきたい実測(land を止める判断は依頼者にお任せします)

残り 1 本 = test_herdr_duplicate_session_rejected が、全量走行時に断続的に赤になります(観測 6 回中 3 回赤 / 3 回緑。単独走行は 3 回とも緑、単体指定の連続走行も load 85〜91 で 3 回とも緑)。

診断メッセージを一時的に入れて失敗点を特定したところ、段(2)「名札消失後の重複」で tmux-new-session が duplicate を raise しなかった(DIAG-3)ものでした。段(1)の通常の重複拒否は通っています。つまり create-then-verify の判定時に、先行 session の label 保持者が名簿から見えなかったことになります。

原因の候補を 2 つ実測で潰しましたが、まだ特定できていません:

  • 実運用側による刈り取り: deftest と同じ API 列(pane.report_agent で claude/working を名乗る + agent.rename)を打った workspace が外部に閉じられるか 90 秒観測 → 閉じられない(否定)。
  • workspace_id の順序鍵: 12 連続 create で shortlex 順 = 創出順が成立(否定)。

いずれにせよ main は名札消失後の重複を一切拒否できない(依存していた agent_name_taken が名札と一緒に消えるため)ので、本 PR は現状より後退はしません。ただし本 PR が導入する不変量そのものが実走行で 1 度すり抜けた実測なので、land 後の follow-up として残すか land 前に詰めるかは判断が要ると考えます。再現条件は「全量走行 + 高負荷(load 76〜130)」で、静穏機での再測が有効だと思います。

proboscis added a commit that referenced this pull request Aug 13, 2026
## 何を直すか

#587 の重複 session 拒否(create-then-verify)は「workspace_id は創出順に
単調増加するので id 最小 = 先行 session」という前提に依存していた。この前提は
成立しない。

実測(2026-08-14・稼働 herdr 0.7.5 / protocol 17):
- 断続的に赤だった全量走行の RPC 記録で、連続 create が w3NZ → w3N0
- 独立の連続 create 実測でも 200 回中 1 回 w3MZ → w3M0

いずれも後発のほうが shortlex で先に来る。前提が破れると gate は後発を勝者に
選び、**重複 session がそのまま素通り**する。deftest
test-herdr-duplicate-session-rejected が全量走行でのみ断続的に赤だった
(観測 6 回中 3 回)のはこれで、単独走行は反転を踏まないため緑だった。
初版の根拠(2026-08-09 の w1VS → w1VT → w1VV)は末尾桁の繰り上がりを跨いで
おらず、一般には成立していなかった。

## どう直すか

判定を 2 段に分け、既存 session の検出を id の順序から切り離す
(新設の純関数 herdr-new-session-verdict が規則の単一定義点):

- 段 1 = 事前照会。workspace を作る前に label 保持者を数え、1 つでも居れば
  作らずに拒否する。順序に一切依存しない。無駄な生成と、close 失敗時の
  label 二重保持も同時に消える。
- 段 2 = 事後照合。段 1 と作成の間に割り込んだ同時作成だけが残る。競合者は
  同じ名簿から同じ全順序で同じ勝者に合意するので、ちょうど 1 つが残る
  (合意に要るのは全順序であって創出順ではない)。
- 作成直後に自分が名簿に居ない場合は "vanished" として重複と別に名乗る。
  旧実装は空 list に (get holders 0) を当てて IndexError になり、呼び手の
  RuntimeError 捕捉を素通りしていた。

shortlex 鍵(herdr-workspace-order-key)は残すが、用途を「同時作成の決定的
合意」だけに限定し、創出順の主張を docstring から撤回した。

## 検証

- 新設 deftest test-herdr-new-session-verdict-ignores-id-order(実測の反転形
  w3MZ / w3M0 を含む 6 例)— 実装前は red、実装後 green
- packages/doeff-agents/tests/test_sessionhost_substrate_herdr.py = 14 passed
- 実装経路の連続実射 200 回 = 重複拒否 200 / すり抜け 0
- 規則の対比(実測の反転形): 旧規則 = ok(素通り)/ 新規則 = duplicate
- conformance/herdr-physics.md の該当節を訂正(旧「単調増加」の記述を反例つきで
  差し替え)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@proboscis

Copy link
Copy Markdown
Owner Author

すり抜けの根本原因を特定しました — 前提が実測で偽でした(修正同梱)

先の comment で「全量走行時に断続的にすり抜ける(原因未特定)」と報告した件の追報です。原因が判明したので、直して同梱しました(27ba33cd)。

原因: workspace_id は創出順に単調ではない

本 PR の重複判定は create-then-verify で、勝敗を workspace_id の shortlex 順 = 創出順 で決めていました。この前提が成立しません。

実測(2026-08-14・稼働 herdr 0.7.5 / protocol 17):

出所 観測
断続的に赤だった全量走行の RPC 記録(実失敗時) 連続 create が w3NZw3N0
独立の連続 create 実射(200 回) 200 回中 1 回 w3MZw3M0

いずれも後発のほうが shortlex で先に来ます。前提が破れた瞬間、gate は後発を勝者に選び、重複 session がそのまま素通りします。

deftest test-herdr-duplicate-session-rejected が全量走行でのみ断続的に赤(観測 6 回中 3 回)で、単独走行では 3 回とも緑だったのはこれです — 単独走行は id の反転を踏みません。捕捉した失敗点も段(2)の「名札消失後の重複が拒否されなかった」で一致します。

初版の根拠(2026-08-09 の w1VS → w1VT → w1VV)は末尾桁の繰り上がりを跨いでいない 3 点で、一般には成立していませんでした。

否定した候補も記録しておきます(いずれも実測):外部プロセスによる workspace の刈り取り(90 秒 + 600 秒観測で不変)/ workspace label の自動改名(同)/ 兄弟 workspace の close 巻き添え(直接実射)/ workspace.list の一時的欠落・切り詰め(1,182 回 poll で 0 件)。

直し方: 既存 session の検出を id の順序から切り離す

新設の純関数 herdr-new-session-verdict を規則の単一定義点にして、判定を 2 段に分けました。

  • 段 1 = 事前照会: workspace を作る前に label 保持者を数え、1 つでも居れば作らずに拒否する。順序に一切依存しません。副次的に、無駄な workspace 生成と、close 失敗時に label が二重保持される経路も消えます。
  • 段 2 = 事後照合: 段 1 と作成の間に割り込んだ同時作成だけが残ります。競合者は同じ名簿から同じ全順序で同じ勝者に合意するので、ちょうど 1 つが残ります(合意に要るのは全順序であって創出順ではありません)。
  • 作成直後に自分が名簿に居ない場合は "vanished" として重複と別に名乗ります。旧実装は空 list に (get holders 0) を当てて IndexError になり、呼び手の RuntimeError 捕捉を素通りしていました。

herdr-workspace-order-key は残しますが、用途を「同時作成の決定的合意」だけに限定し、創出順の主張を docstring から撤回しました。conformance/herdr-physics.md の該当節も反例つきで訂正しています。

検証

  • 新設 deftest test-herdr-new-session-verdict-ignores-id-order(実測の反転形 w3MZ / w3M0 を含む 6 例)— 実装前 red / 実装後 green
  • 規則の対比(実測の反転形): 旧規則 = ok(素通り)/ 新規則 = duplicate
  • 実装経路の連続実射 200 回 = 重複拒否 200 / すり抜け 0
  • test_sessionhost_substrate_herdr.py 単独 = 14 passed
  • packages/doeff-agents/tests 全量 -m 'not e2e'3 回連続 = いずれも 6 failed / 1817 passed / 21 skipped。赤 6 本は test_sessionhost_resume_cross_binding.py のみで、現 main のコードでも同じ 6 本が赤(実測済み)。herdr substrate は 3 回とも全緑で、以前の断続的な赤は再現しません。
  • tests/test_enforcement_ledger.py + tests/semgrep/test_vm_failfast_semgrep_rules.py = 18 passed
  • ruff check packages/doeff-agents/ の残り 2 件は test_provider_failure_markers.py の既存指摘(本枝を退避しても同数)
  • semgrep(変更 2 ファイル)= 指摘なし

計測時の load average は 39〜77 でした(赤を観測していた時間帯は 100〜220)。ただし段 1 は順序にも時間にも依存しない無条件の拒否なので、負荷条件によらず成立します。

別件の所見(本 PR の射程外・現 main にも在る)

同じ調査中に、herdr-pane-current-command-iopane.process_info の foreground process に argv0 が無い応答で KeyError: 'argv0' になるのを実走行で 1 度観測しました(test_herdr_lifecycle_smoke が赤)。docstring は「foreground 不明は None」と契約しているので、契約どおり None を返すべき箇所です。現 main の同コードにも同じ形があり本 PR の変更点ではないため、ここでは直していません。別途の起票が要ると考えます。

proboscis and others added 5 commits August 14, 2026 20:14
herdr 0.7.5 / protocol 17 で agent.start が {name, kind, pane_id} 必須の
「既存 pane への管理対象 agent 起動 + 検出待ち」へ改形され(bundled schema
$defs.AgentStartParams、稼働 server 実測とも一致)、旧 payload はまず
missing field `kind` で拒否される(kind を足しても missing field pane_id —
実測)。shell pane の名前付き生成という TmuxNewSession の用途には
もう使えないため、herdr-new-session-io を protocol 17 の経路へ束縛替え:

  workspace.create {label, cwd, env, focus:false}(cwd/env を直接受ける
  ようになった — root pane がそのまま session pane、全幅 grid 維持)
  → pane.report_agent(外部 authority で agent エントリ生成)
  → agent.rename(名簿登録 — 重複名は agent_name_taken ネイティブ拒否
    = tmux duplicate 拒否 parity 維持)
  → pane.clear_agent_authority(state authority を画面検出へ返却 —
    名前は terminal に残り agent.get で解決可能、実測)

kill parity 不変(唯一 pane の pane.close で workspace 自動消滅 + 名簿解放)。
tmux backend(substrate.hy / host.hy)は無変更。実測記録は
conformance/herdr-physics.md の追補に記載。

test_sessionhost_substrate_herdr.py: 5 failed / 6 passed → 11 passed。

Refs #556

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…TDD red)

issue substrate-herdr-session-identity-anchor-r2-607f0c(#556 の系譜、PR #569 の refile 先)。

実測前提(probe 2026-08-01 n=3 / 2026-08-09):
- pane 内で実 agent を起動すると ~2 秒で herdr の実 agent 検出が agent 名札を
  上書きし、agent.get {target: session 名} が agent_not_found になる
- workspace label は実 agent 起動後も残存し、workspace.list で解決可能
- herdr は同一 label の workspace 重複をネイティブ拒否しない
- workspace_id は base62 風カウンタで創出順に単調増加(shortlex 比較で順序復元)
- agent 名には invalid_agent_name 制約(小文字開始・[a-z0-9_-]・32 文字以内)が
  あるが workspace label は無制約(60 文字・大文字・記号を実測受理)

TDD red(現行実装での失敗を確認済み):
- test-herdr-identity-survives-agent-name-loss: 名札消失模擬後の
  has-session/session-pane-ids/capture/send/kill 全経路 —
  「has-session must survive agent name-plate loss」で fail
- test-herdr-duplicate-session-rejected(改訂): doeff 側 duplicate 判定 +
  名札消失後の重複素通り検出 + 敗者 workspace 掃除 — agent_name_taken 依存の
  現行実装で fail
- test-herdr-workspace-order-key-shortlex: tie-break 順序鍵 — ImportError で fail

semgrep: doeff-agents-herdr-session-identity-not-agent-name("agent.get" 解決の
恒久禁止、sessionhost + conformance)。既存コード 3 箇所で発火を確認。
enforcement-ledger semgrep_rules 247→248。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
issue substrate-herdr-session-identity-anchor-r2-607f0c(#556 の系譜)。
PR #569 の workspace.create {label cwd env} 経路を出発点に、agent 名簿への
名前登録(pane.report_agent → agent.rename → pane.clear_agent_authority)を
撤去し、生死確認・帰属観測・kill の名前解決を workspace label に束縛替え:

- herdr-workspace-order-key: workspace_id の創出順比較鍵(shortlex —
  base62 風カウンタの単調増加を実測 2026-08-09)
- herdr-label-workspace-ids-io / herdr-workspace-id-io: label → workspace
  解決(TmuxHasSession / TmuxSessionPaneIds / TmuxKillSession が共有)
- herdr-new-session-io: create-then-verify の重複 gate(herdr は label 重複を
  ネイティブ拒否しない — 実測 2026-08-09。TOCTOU 窓を閉じる根拠はコード近傍)
- herdr-kill-session-io: workspace.close(全 pane 破棄 = tmux kill-session
  parity、S19c 型 sibling も取り残さない)
- conformance/harness.py: 帯域外経路(kill / liveness / S19c fault
  injection / adopt fixture)を同アンカーへ移行。create_session_out_of_band
  の protocol 14 形 agent.start 残骸も workspace.create 直行に修正
- herdr-physics.md:「clear_agent_authority 後も名前が残る」の破れ訂正
  (実 agent 起動で ~2 秒後に検出が名札上書き — 実測 2026-08-01 n=3)+
  アンカー移行の追補

テスト: test_sessionhost_substrate_herdr.py 13 passed(TDD red 3 本を含む)。
semgrep doeff-agents-herdr-session-identity-not-agent-name 0 findings。
tmux backend(substrate.hy / tmux.py)は無変更。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## 何を直すか

#587 の重複 session 拒否(create-then-verify)は「workspace_id は創出順に
単調増加するので id 最小 = 先行 session」という前提に依存していた。この前提は
成立しない。

実測(2026-08-14・稼働 herdr 0.7.5 / protocol 17):
- 断続的に赤だった全量走行の RPC 記録で、連続 create が w3NZ → w3N0
- 独立の連続 create 実測でも 200 回中 1 回 w3MZ → w3M0

いずれも後発のほうが shortlex で先に来る。前提が破れると gate は後発を勝者に
選び、**重複 session がそのまま素通り**する。deftest
test-herdr-duplicate-session-rejected が全量走行でのみ断続的に赤だった
(観測 6 回中 3 回)のはこれで、単独走行は反転を踏まないため緑だった。
初版の根拠(2026-08-09 の w1VS → w1VT → w1VV)は末尾桁の繰り上がりを跨いで
おらず、一般には成立していなかった。

## どう直すか

判定を 2 段に分け、既存 session の検出を id の順序から切り離す
(新設の純関数 herdr-new-session-verdict が規則の単一定義点):

- 段 1 = 事前照会。workspace を作る前に label 保持者を数え、1 つでも居れば
  作らずに拒否する。順序に一切依存しない。無駄な生成と、close 失敗時の
  label 二重保持も同時に消える。
- 段 2 = 事後照合。段 1 と作成の間に割り込んだ同時作成だけが残る。競合者は
  同じ名簿から同じ全順序で同じ勝者に合意するので、ちょうど 1 つが残る
  (合意に要るのは全順序であって創出順ではない)。
- 作成直後に自分が名簿に居ない場合は "vanished" として重複と別に名乗る。
  旧実装は空 list に (get holders 0) を当てて IndexError になり、呼び手の
  RuntimeError 捕捉を素通りしていた。

shortlex 鍵(herdr-workspace-order-key)は残すが、用途を「同時作成の決定的
合意」だけに限定し、創出順の主張を docstring から撤回した。

## 検証

- 新設 deftest test-herdr-new-session-verdict-ignores-id-order(実測の反転形
  w3MZ / w3M0 を含む 6 例)— 実装前は red、実装後 green
- packages/doeff-agents/tests/test_sessionhost_substrate_herdr.py = 14 passed
- 実装経路の連続実射 200 回 = 重複拒否 200 / すり抜け 0
- 規則の対比(実測の反転形): 旧規則 = ok(素通り)/ 新規則 = duplicate
- conformance/herdr-physics.md の該当節を訂正(旧「単調増加」の記述を反例つきで
  差し替え)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
前段(27ba33c)は重複 gate から順序前提を外したが、**名前解決の側に同じ前提が
残っていた**: herdr-workspace-id-io が保持者一覧の先頭を「その名前の session」と
して選び、生死・帰属観測・kill がその 1 つに向いていた。workspace_id は創出順に
単調でない(前段の実測 w3NZ → w3N0 / 追試 2026-08-14 の
w3ZW → w3ZX → w3ZY → w3ZZ → …繰り上がり… → w303 → w304、および review 実測
w35Y → w35Z → w350 → w351)ので、この選択は任意の保持者を掴む。

**前段(27ba33c)に対する実測反例**(同 label の holder 2 つ — 同時作成の敗者が
閉じる前・敗者の close 失敗・doeff 外の同名 label・conformance の帯域外 create で
成立する状態):
    holders ['w42F','w42G'] → 帰属観測 ['w42F:p1'](第 2 holder の pane が不可視)
    → kill 後の holders ['w42G'](生きた workspace の漏れ。呼び手には「殺した」)
帰属観測の穴は policy.hy の R4 検証(pane が session に属さない → vanished 即時終端)
を通って、**生きた席の誤終端**になる。

**修理**(順序を要求しない形にする — herdr に創出順が無いことが破れに繋がらない):
  - 生死(TmuxHasSession)= 保持者が非空か。
  - 帰属観測(TmuxSessionPaneIds)= 全保持者の pane.list の和(tmux
    `list-panes -s -t NAME` = その名前に帰属する pane 全部、の parity)。
  - kill(TmuxKillSession)= 全保持者を workspace.close(その名前の session が
    消える、の parity)。消滅済み保持者の workspace_not_found のみ飲み込む
    (実測 2026-08-14: 消えた workspace への pane.list / workspace.close は
    この code を返す)。
  - herdr-workspace-id-io(「その 1 つ」を選ぶ面)は撤去。純関数
    herdr-label-holders を新設し、保持者は並べ替えずに集合として返す。
  - 診断文言も保持者を全部並べる(1 つ選んで名乗ると「それが session」と読める)。
  - conformance/harness.py も同形へ — 反証済みの創出順鍵 _herdr_workspace_order_key
    を撤去(review 指摘の「両方に適用せよ」の対)。kill = 全保持者掃き取り、
    liveness = 非空判定は元から順序に依存しない。
  - 前段の 2 段 gate(事前照会 + 同時作成の合意)と shortlex 鍵の用途限定は
    そのまま活かす — 合意に要るのは全順序であって創出順ではない、という前段の
    整理は正しい。

**恒久ガード**: semgrep doeff-agents-herdr-label-holders-must-not-be-indexed
(substrate_herdr.hy / conformance harness.py での保持者一覧の添字参照を禁止)
+ probe fixture + 単体テスト。enforcement 台帳 249 → 250。

**テスト**(実 herdr server、canonical 16 passed):
  - test-herdr-label-holders-are-a-set-in-listing-order(純関数・繰り上がり境界
    w35Z/w350 の反例つき — 字面の順序鍵は先行/後発を入れ替える)
  - test-herdr-name-resolution-covers-every-label-holder(保持者 2 つで帰属観測の
    被覆と kill の全掃き取り。前段の実装では red = 上記の実測反例)
  - test-herdr-duplicate-session-rejected を強化(残存する 1 つが先行の workspace
    であること・名前解決が先行 pane を指し続けること)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@proboscis

Copy link
Copy Markdown
Owner Author

本 PR は #594(wt/herdr17-substrate-followup)に丸ごと含めて origin/main(050025e)へ載せ直しました(merge commit 33e335b・衝突は enforcement-ledger.json の実数 1 件のみ)。#594 は加えて、label だけの has-session が `ai` 等の外部命名席(herdr agent 名簿の名前 = koine session.adopt の session_name・workspace label は repo 名)を全部「不在」と答え adopt を全拒否 / substrate_present 一斉 false にする未検出回帰を、名簿の実在確認 1 点(semgrep waiver marker 直下)で塞いでいます。#594 が land したら本 PR は superseded として close します(land は operator 承認 → integration-lead 手運び)。

🤖 Generated with Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant